@shohojdhara/atomix 0.6.3 → 0.6.5
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 +119 -40
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +1 -1
- package/dist/atomix.min.css.map +1 -1
- package/dist/atomix.umd.js +1 -1
- package/dist/atomix.umd.js.map +1 -1
- package/dist/atomix.umd.min.js +1 -1
- package/dist/charts.d.ts +30 -1
- package/dist/charts.js +566 -597
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +30 -1
- package/dist/core.js +600 -624
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +30 -1
- package/dist/forms.js +1122 -1163
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +31 -89
- package/dist/heavy.js +1015 -1045
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +378 -104
- package/dist/index.esm.js +10959 -10837
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +10935 -10812
- 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/Accordion/Accordion.tsx +2 -5
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +14 -16
- package/src/components/AtomixGlass/AtomixGlass.tsx +137 -355
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +32 -249
- package/src/components/AtomixGlass/GlassFilter.tsx +62 -68
- package/src/components/AtomixGlass/README.md +2 -1
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +19 -18
- package/src/components/AtomixGlass/glass-border-styles.test.ts +58 -0
- package/src/components/AtomixGlass/glass-border-styles.ts +136 -0
- package/src/components/AtomixGlass/glass-utils.ts +411 -6
- package/src/components/AtomixGlass/stories/AnimationFeatures.stories.tsx +158 -537
- package/src/components/AtomixGlass/stories/Border.stories.tsx +149 -0
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +229 -89
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +29 -340
- package/src/components/AtomixGlass/stories/argTypes.ts +30 -13
- package/src/components/AtomixGlass/stories/premium-presets.ts +206 -0
- package/src/components/AtomixGlass/stories/shared-components.tsx +52 -8
- package/src/components/Badge/Badge.tsx +4 -4
- package/src/components/Button/Button.tsx +2 -6
- package/src/components/Callout/Callout.test.tsx +4 -3
- package/src/components/Callout/Callout.tsx +2 -5
- package/src/components/Dropdown/Dropdown.tsx +3 -7
- package/src/components/Form/Checkbox.tsx +2 -8
- package/src/components/Form/Input.tsx +2 -9
- package/src/components/Form/Radio.tsx +2 -9
- package/src/components/Form/Select.tsx +2 -7
- package/src/components/Form/Textarea.tsx +2 -9
- package/src/components/Messages/Messages.tsx +2 -8
- package/src/components/Modal/Modal.tsx +4 -5
- package/src/components/Navigation/Nav/Nav.tsx +2 -6
- package/src/components/Navigation/Navbar/Navbar.tsx +2 -9
- package/src/components/Navigation/SideMenu/SideMenu.tsx +2 -6
- package/src/components/Pagination/Pagination.tsx +2 -10
- package/src/components/Popover/Popover.tsx +2 -9
- package/src/components/Progress/Progress.tsx +2 -7
- package/src/components/Rating/Rating.tsx +2 -10
- package/src/components/Spinner/Spinner.tsx +2 -7
- package/src/components/Steps/Steps.tsx +2 -10
- package/src/components/Tabs/Tabs.tsx +2 -9
- package/src/components/Toggle/Toggle.tsx +2 -10
- package/src/components/Tooltip/Tooltip.tsx +2 -5
- package/src/lib/composables/useAtomixGlass.ts +41 -10
- package/src/lib/composables/useAtomixGlassStyles.ts +59 -75
- package/src/lib/composables/usePerformanceMonitor.ts +5 -0
- package/src/lib/constants/components.ts +358 -46
- package/src/lib/types/components.ts +33 -1
- package/src/styles/01-settings/_settings.atomix-glass.scss +69 -31
- package/src/styles/02-tools/_tools.glass.scss +45 -3
- package/src/styles/06-components/_components.atomix-glass.scss +114 -77
- package/src/components/AtomixGlass/deprecated/AtomixGlass.deprecated.tsx +0 -390
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CSSProperties } from 'react';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import type {
|
|
3
|
+
import type { DisplacementMode, GlassSize, MousePosition } from '../../lib/types/components';
|
|
4
4
|
import { ATOMIX_GLASS } from '../../lib/constants/components';
|
|
5
5
|
|
|
6
6
|
const { CONSTANTS } = ATOMIX_GLASS;
|
|
@@ -44,7 +44,7 @@ export const calculateMouseInfluence = (mouseOffset: MousePosition): number => {
|
|
|
44
44
|
if (!mouseOffset || typeof mouseOffset.x !== 'number' || typeof mouseOffset.y !== 'number') {
|
|
45
45
|
return 0;
|
|
46
46
|
}
|
|
47
|
-
//
|
|
47
|
+
// Clamp influence to keep mouse response subtle and stable.
|
|
48
48
|
const influence =
|
|
49
49
|
Math.sqrt(mouseOffset.x * mouseOffset.x + mouseOffset.y * mouseOffset.y) /
|
|
50
50
|
CONSTANTS.MOUSE_INFLUENCE_DIVISOR;
|
|
@@ -280,6 +280,409 @@ export const calculateVelocity = (
|
|
|
280
280
|
return (current - previous) / deltaTime;
|
|
281
281
|
};
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Layout, sizing, and effect-resolution utilities for AtomixGlass.
|
|
285
|
+
*
|
|
286
|
+
* The root wrapper exposes CSS custom properties; the container owns layout and
|
|
287
|
+
* backdrop-filter. Helpers in this section keep decorative layers aligned with
|
|
288
|
+
* the container across fixed, sticky, and in-flow positioning modes.
|
|
289
|
+
*/
|
|
290
|
+
|
|
291
|
+
/** Subset of CSS layout properties used for glass positioning. */
|
|
292
|
+
export type GlassLayoutPosition = Pick<
|
|
293
|
+
CSSProperties,
|
|
294
|
+
'position' | 'top' | 'left' | 'right' | 'bottom' | 'inset'
|
|
295
|
+
>;
|
|
296
|
+
|
|
297
|
+
/** Inset values formatted for `--atomix-glass-*` custom properties. */
|
|
298
|
+
export interface GlassLayerInsetVars {
|
|
299
|
+
top: string;
|
|
300
|
+
left: string;
|
|
301
|
+
right: string;
|
|
302
|
+
bottom: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Normalizes a layout inset for use in CSS custom properties.
|
|
307
|
+
*
|
|
308
|
+
* @param value - Raw inset from `style` (number, px string, or `auto`).
|
|
309
|
+
* @param fallback - Value used when `value` is undefined.
|
|
310
|
+
*/
|
|
311
|
+
export function formatGlassInsetValue(
|
|
312
|
+
value: string | number | undefined,
|
|
313
|
+
fallback: string | number = 'auto'
|
|
314
|
+
): string {
|
|
315
|
+
if (value === undefined) {
|
|
316
|
+
return typeof fallback === 'number' ? `${fallback}px` : String(fallback);
|
|
317
|
+
}
|
|
318
|
+
if (value === 'auto') {
|
|
319
|
+
return 'auto';
|
|
320
|
+
}
|
|
321
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Determines whether the glass should use fixed/sticky layout semantics.
|
|
326
|
+
*
|
|
327
|
+
* @param explicit - Value of the `isFixedOrSticky` prop.
|
|
328
|
+
* @param position - `position` from the consumer `style` object.
|
|
329
|
+
*/
|
|
330
|
+
export function isGlassFixedOrSticky(
|
|
331
|
+
explicit?: boolean,
|
|
332
|
+
position?: CSSProperties['position']
|
|
333
|
+
): boolean {
|
|
334
|
+
return Boolean(explicit || position === 'fixed' || position === 'sticky');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Extracts layout-related properties from a React `CSSProperties` object.
|
|
339
|
+
*/
|
|
340
|
+
export function pickGlassLayoutStyle(style: CSSProperties): GlassLayoutPosition {
|
|
341
|
+
const { position, top, left, right, bottom, inset } = style;
|
|
342
|
+
return {
|
|
343
|
+
...(position != null && { position }),
|
|
344
|
+
...(top !== undefined && { top }),
|
|
345
|
+
...(left !== undefined && { left }),
|
|
346
|
+
...(right !== undefined && { right }),
|
|
347
|
+
...(bottom !== undefined && { bottom }),
|
|
348
|
+
...(inset !== undefined && { inset }),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Builds inline layout styles for the root wrapper.
|
|
354
|
+
*
|
|
355
|
+
* Reserved for alternate layout strategies. The default implementation applies
|
|
356
|
+
* layout on `.c-atomix-glass__container` to preserve backdrop-filter behavior.
|
|
357
|
+
*/
|
|
358
|
+
export function pickGlassHoistedRootStyle(style: CSSProperties): CSSProperties {
|
|
359
|
+
const layout = pickGlassLayoutStyle(style);
|
|
360
|
+
const resolvedPosition = (layout.position ?? style.position ?? 'fixed') as CSSProperties['position'];
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
position: resolvedPosition,
|
|
364
|
+
...(layout.top !== undefined && { top: layout.top }),
|
|
365
|
+
...(layout.left !== undefined && { left: layout.left }),
|
|
366
|
+
...(layout.right !== undefined && { right: layout.right }),
|
|
367
|
+
...(layout.bottom !== undefined && { bottom: layout.bottom }),
|
|
368
|
+
...(layout.inset !== undefined && { inset: layout.inset }),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Returns `style` without layout properties, preserving visual declarations only.
|
|
374
|
+
*/
|
|
375
|
+
export function omitGlassLayoutStyle(style: CSSProperties): CSSProperties {
|
|
376
|
+
const {
|
|
377
|
+
position: _p,
|
|
378
|
+
top: _t,
|
|
379
|
+
left: _l,
|
|
380
|
+
right: _r,
|
|
381
|
+
bottom: _b,
|
|
382
|
+
inset: _inset,
|
|
383
|
+
...visualStyle
|
|
384
|
+
} = style;
|
|
385
|
+
return visualStyle;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Resolves inset custom properties for decorative layers (hover, borders, backgrounds).
|
|
390
|
+
*
|
|
391
|
+
* For fixed and sticky modes, insets mirror the container so sibling layers remain
|
|
392
|
+
* aligned. In-flow modes, insets follow the consumer `style` when a non-default
|
|
393
|
+
* `position` is provided.
|
|
394
|
+
*/
|
|
395
|
+
export function getGlassLayerInsetVars(
|
|
396
|
+
isFixedOrSticky: boolean,
|
|
397
|
+
restStyle: CSSProperties
|
|
398
|
+
): GlassLayerInsetVars {
|
|
399
|
+
if (isFixedOrSticky) {
|
|
400
|
+
return {
|
|
401
|
+
top: formatGlassInsetValue(restStyle.top, 0),
|
|
402
|
+
left: formatGlassInsetValue(restStyle.left, 0),
|
|
403
|
+
right: formatGlassInsetValue(restStyle.right, 'auto'),
|
|
404
|
+
bottom: formatGlassInsetValue(restStyle.bottom, 'auto'),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const position = restStyle.position;
|
|
409
|
+
const usesCustomPosition =
|
|
410
|
+
position != null && position !== 'static' && position !== 'relative';
|
|
411
|
+
|
|
412
|
+
if (!usesCustomPosition) {
|
|
413
|
+
return { top: '0px', left: '0px', right: 'auto', bottom: 'auto' };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
top: formatGlassInsetValue(restStyle.top, 0),
|
|
418
|
+
left: formatGlassInsetValue(restStyle.left, 0),
|
|
419
|
+
right: formatGlassInsetValue(restStyle.right, 'auto'),
|
|
420
|
+
bottom: formatGlassInsetValue(restStyle.bottom, 'auto'),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Resolves the `--atomix-glass-position` value for decorative layers.
|
|
426
|
+
*
|
|
427
|
+
* Fixed/sticky layers use the same positioning mode as the container; in-flow
|
|
428
|
+
* layers default to the internal absolute positioning context.
|
|
429
|
+
*/
|
|
430
|
+
export function getGlassLayerPositionVar(
|
|
431
|
+
isFixedOrSticky: boolean,
|
|
432
|
+
positionStyles: GlassLayoutPosition,
|
|
433
|
+
restStyle: CSSProperties
|
|
434
|
+
): string {
|
|
435
|
+
if (!isFixedOrSticky) {
|
|
436
|
+
return `${positionStyles.position}`;
|
|
437
|
+
}
|
|
438
|
+
const layout = pickGlassLayoutStyle(restStyle);
|
|
439
|
+
return `${layout.position ?? restStyle.position ?? 'fixed'}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Returns the internal positioning context for effect layers relative to the root.
|
|
444
|
+
*/
|
|
445
|
+
export function getGlassInternalPositionStyles(
|
|
446
|
+
isFixedOrSticky: boolean,
|
|
447
|
+
restStyle: CSSProperties
|
|
448
|
+
): GlassLayoutPosition {
|
|
449
|
+
return {
|
|
450
|
+
position: (isFixedOrSticky
|
|
451
|
+
? 'absolute'
|
|
452
|
+
: restStyle.position || 'absolute') as CSSProperties['position'],
|
|
453
|
+
top: 0,
|
|
454
|
+
left: 0,
|
|
455
|
+
right: 'auto',
|
|
456
|
+
bottom: 'auto',
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Computes `--atomix-glass-width` and `--atomix-glass-height` values.
|
|
462
|
+
*
|
|
463
|
+
* Fixed/sticky elements prefer explicit dimensions or measured size; in-flow
|
|
464
|
+
* elements default to `100%`.
|
|
465
|
+
*/
|
|
466
|
+
export function resolveGlassAdjustedSize(options: {
|
|
467
|
+
width?: string | number;
|
|
468
|
+
height?: string | number;
|
|
469
|
+
restStyle: CSSProperties;
|
|
470
|
+
glassSize: GlassSize;
|
|
471
|
+
isFixedOrSticky: boolean;
|
|
472
|
+
}): { width: string; height: string } {
|
|
473
|
+
const { width, height, restStyle, glassSize, isFixedOrSticky } = options;
|
|
474
|
+
|
|
475
|
+
const resolveLength = (value: string | number | undefined, measured: number): string => {
|
|
476
|
+
if (value !== undefined && isFixedOrSticky) {
|
|
477
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
478
|
+
}
|
|
479
|
+
if (measured > 0 && isFixedOrSticky) {
|
|
480
|
+
return `${measured}px`;
|
|
481
|
+
}
|
|
482
|
+
return '100%';
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const effectiveWidth = width ?? restStyle.width;
|
|
486
|
+
const effectiveHeight = height ?? restStyle.height;
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
width: resolveLength(effectiveWidth, glassSize.width),
|
|
490
|
+
height: resolveLength(effectiveHeight, glassSize.height),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Input parameters for {@link buildGlassRootCssVariables}. */
|
|
495
|
+
export interface GlassRootCssVarsInput {
|
|
496
|
+
effectiveBorderRadius: number;
|
|
497
|
+
transformStyle?: string;
|
|
498
|
+
adjustedSize: { width: string; height: string };
|
|
499
|
+
isOverLight: boolean;
|
|
500
|
+
customZIndex?: string | number;
|
|
501
|
+
isFixedOrSticky: boolean;
|
|
502
|
+
positionStyles: GlassLayoutPosition;
|
|
503
|
+
restStyle: CSSProperties;
|
|
504
|
+
/** Rim width — maps to `--atomix-glass-border-width`. */
|
|
505
|
+
borderWidth?: string;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Builds the CSS custom properties applied to the root `.c-atomix-glass` element.
|
|
510
|
+
*
|
|
511
|
+
* These variables drive layer geometry, transforms, and stacking offsets. They
|
|
512
|
+
* must not include layout properties that would interfere with backdrop-filter.
|
|
513
|
+
*/
|
|
514
|
+
export function buildGlassRootCssVariables(input: GlassRootCssVarsInput): CSSProperties {
|
|
515
|
+
const {
|
|
516
|
+
effectiveBorderRadius,
|
|
517
|
+
transformStyle,
|
|
518
|
+
adjustedSize,
|
|
519
|
+
isOverLight,
|
|
520
|
+
customZIndex,
|
|
521
|
+
isFixedOrSticky,
|
|
522
|
+
positionStyles,
|
|
523
|
+
restStyle,
|
|
524
|
+
borderWidth = ATOMIX_GLASS.BORDER.DEFAULT_WIDTH,
|
|
525
|
+
} = input;
|
|
526
|
+
|
|
527
|
+
const layerPosition = getGlassLayerPositionVar(isFixedOrSticky, positionStyles, restStyle);
|
|
528
|
+
const layerInsets = getGlassLayerInsetVars(isFixedOrSticky, restStyle);
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
...(customZIndex !== undefined && { '--atomix-glass-base-z-index': customZIndex }),
|
|
532
|
+
'--atomix-glass-radius': `${effectiveBorderRadius}px`,
|
|
533
|
+
'--atomix-glass-transform': transformStyle || 'none',
|
|
534
|
+
'--atomix-glass-container-position': layerPosition,
|
|
535
|
+
'--atomix-glass-position': layerPosition,
|
|
536
|
+
'--atomix-glass-top': layerInsets.top,
|
|
537
|
+
'--atomix-glass-left': layerInsets.left,
|
|
538
|
+
'--atomix-glass-right': layerInsets.right,
|
|
539
|
+
'--atomix-glass-bottom': layerInsets.bottom,
|
|
540
|
+
'--atomix-glass-width': adjustedSize.width,
|
|
541
|
+
'--atomix-glass-height': adjustedSize.height,
|
|
542
|
+
// Aliases maintained for backward compatibility and consumer overrides.
|
|
543
|
+
'--atomix-glass-container-width': adjustedSize.width,
|
|
544
|
+
'--atomix-glass-container-height': adjustedSize.height,
|
|
545
|
+
[ATOMIX_GLASS.BORDER.WIDTH_CSS_VAR]: borderWidth,
|
|
546
|
+
'--atomix-glass-blend-mode': isOverLight ? 'multiply' : 'overlay',
|
|
547
|
+
} as CSSProperties;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Resolved visual effect values passed to {@link AtomixGlassContainer}. */
|
|
551
|
+
export interface ResolvedGlassContainerEffects {
|
|
552
|
+
displacementScale: number;
|
|
553
|
+
blurAmount: number;
|
|
554
|
+
saturation: number;
|
|
555
|
+
aberrationIntensity: number;
|
|
556
|
+
mouseOffset: MousePosition;
|
|
557
|
+
globalMousePosition: MousePosition;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Applies mode-specific multipliers and accessibility overrides to container effects.
|
|
562
|
+
*/
|
|
563
|
+
export function resolveGlassContainerEffects(options: {
|
|
564
|
+
displacementScale: number;
|
|
565
|
+
blurAmount: number;
|
|
566
|
+
saturation: number;
|
|
567
|
+
aberrationIntensity: number;
|
|
568
|
+
mode: DisplacementMode;
|
|
569
|
+
effectiveWithoutEffects: boolean;
|
|
570
|
+
effectiveHighContrast: boolean;
|
|
571
|
+
isOverLight: boolean;
|
|
572
|
+
saturationBoost: number;
|
|
573
|
+
mouseOffset: MousePosition;
|
|
574
|
+
globalMousePosition: MousePosition;
|
|
575
|
+
}): ResolvedGlassContainerEffects {
|
|
576
|
+
const { MULTIPLIERS, SATURATION } = ATOMIX_GLASS.CONSTANTS;
|
|
577
|
+
const zeroMouse: MousePosition = { x: 0, y: 0 };
|
|
578
|
+
|
|
579
|
+
const resolveSaturation = (): number => {
|
|
580
|
+
if (options.effectiveHighContrast) {
|
|
581
|
+
return SATURATION.HIGH_CONTRAST;
|
|
582
|
+
}
|
|
583
|
+
if (options.isOverLight) {
|
|
584
|
+
return options.saturation * options.saturationBoost;
|
|
585
|
+
}
|
|
586
|
+
return options.saturation;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
if (options.effectiveWithoutEffects) {
|
|
590
|
+
return {
|
|
591
|
+
displacementScale: 0,
|
|
592
|
+
blurAmount: 0,
|
|
593
|
+
saturation: resolveSaturation(),
|
|
594
|
+
aberrationIntensity: 0,
|
|
595
|
+
mouseOffset: zeroMouse,
|
|
596
|
+
globalMousePosition: zeroMouse,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
let resolvedDisplacement = options.displacementScale;
|
|
601
|
+
if (options.mode === 'shader') {
|
|
602
|
+
resolvedDisplacement *= MULTIPLIERS.SHADER_DISPLACEMENT;
|
|
603
|
+
} else if (options.isOverLight) {
|
|
604
|
+
resolvedDisplacement *= MULTIPLIERS.OVER_LIGHT_DISPLACEMENT;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let resolvedAberration = options.aberrationIntensity;
|
|
608
|
+
if (options.mode === 'shader') {
|
|
609
|
+
resolvedAberration *= MULTIPLIERS.SHADER_ABERRATION;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
displacementScale: resolvedDisplacement,
|
|
614
|
+
blurAmount: options.blurAmount,
|
|
615
|
+
saturation: resolveSaturation(),
|
|
616
|
+
aberrationIntensity: resolvedAberration,
|
|
617
|
+
mouseOffset: options.mouseOffset,
|
|
618
|
+
globalMousePosition: options.globalMousePosition,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Coerces a value to a finite number, returning `fallback` when invalid. */
|
|
623
|
+
export function toSafeNumber(value: unknown, fallback = 0): number {
|
|
624
|
+
return typeof value === 'number' && !isNaN(value) ? value : fallback;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export type DistortionQuality = 'low' | 'medium' | 'high' | 'ultra';
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Calculates the target frame rate for shader time-animation loops.
|
|
631
|
+
*
|
|
632
|
+
* Balances visual quality against distortion complexity and `animationSpeed`.
|
|
633
|
+
*/
|
|
634
|
+
export function getShaderAnimationTargetFps(options: {
|
|
635
|
+
distortionQuality: DistortionQuality | string;
|
|
636
|
+
animationSpeed?: number;
|
|
637
|
+
withMultiLayerDistortion?: boolean;
|
|
638
|
+
distortionOctaves?: number;
|
|
639
|
+
distortionLacunarity?: number;
|
|
640
|
+
distortionGain?: number;
|
|
641
|
+
}): number {
|
|
642
|
+
const {
|
|
643
|
+
distortionQuality,
|
|
644
|
+
animationSpeed = 1,
|
|
645
|
+
withMultiLayerDistortion,
|
|
646
|
+
distortionOctaves = 3,
|
|
647
|
+
distortionLacunarity = 2,
|
|
648
|
+
distortionGain = 0.5,
|
|
649
|
+
} = options;
|
|
650
|
+
|
|
651
|
+
const baseFps =
|
|
652
|
+
distortionQuality === 'ultra'
|
|
653
|
+
? 60
|
|
654
|
+
: distortionQuality === 'high'
|
|
655
|
+
? 30
|
|
656
|
+
: distortionQuality === 'medium'
|
|
657
|
+
? 24
|
|
658
|
+
: 20;
|
|
659
|
+
|
|
660
|
+
const effectiveSpeed = Math.max(0.5, Math.min(2, animationSpeed));
|
|
661
|
+
const complexity = withMultiLayerDistortion
|
|
662
|
+
? Math.max(
|
|
663
|
+
1,
|
|
664
|
+
distortionOctaves / 3 +
|
|
665
|
+
Math.max(0, distortionLacunarity - 2) * 0.25 +
|
|
666
|
+
Math.max(0, distortionGain - 0.5)
|
|
667
|
+
)
|
|
668
|
+
: 1;
|
|
669
|
+
|
|
670
|
+
return Math.max(12, Math.min(60, Math.round((baseFps * effectiveSpeed) / complexity)));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Computes per-channel displacement scale for the SVG chromatic-aberration filter.
|
|
675
|
+
*/
|
|
676
|
+
export function getChromaticDisplacementScale(
|
|
677
|
+
mode: DisplacementMode,
|
|
678
|
+
displacementScale: number,
|
|
679
|
+
aberrationIntensity: number,
|
|
680
|
+
channelFactor: number
|
|
681
|
+
): number {
|
|
682
|
+
const sign = mode === 'shader' ? 1 : -1;
|
|
683
|
+
return displacementScale * (sign - aberrationIntensity * channelFactor);
|
|
684
|
+
}
|
|
685
|
+
|
|
283
686
|
/**
|
|
284
687
|
* Get displacement map URL based on mode
|
|
285
688
|
*/
|
|
@@ -305,10 +708,12 @@ export const getDisplacementMap = (
|
|
|
305
708
|
}
|
|
306
709
|
};
|
|
307
710
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
711
|
+
/**
|
|
712
|
+
* Module-level LRU cache for shader displacement maps.
|
|
713
|
+
*
|
|
714
|
+
* Shared across all `AtomixGlassContainer` instances so identical size and
|
|
715
|
+
* variant combinations are generated once.
|
|
716
|
+
*/
|
|
312
717
|
export const MAX_CACHE_SIZE = 15;
|
|
313
718
|
|
|
314
719
|
export interface ShaderCacheEntry {
|