@shohojdhara/atomix 0.6.4 → 0.6.6
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 +117 -38
- 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 +625 -846
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +30 -1
- package/dist/core.js +659 -873
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +30 -1
- package/dist/forms.js +1171 -1402
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +31 -89
- package/dist/heavy.js +975 -1195
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +383 -140
- package/dist/index.esm.js +1567 -1679
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1556 -1667
- 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 -364
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +32 -251
- 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 +456 -22
- package/src/components/AtomixGlass/shader-utils.ts +19 -77
- 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.test.tsx +6 -6
- package/src/components/Form/Select.tsx +2 -7
- package/src/components/Form/Textarea.stories.tsx +5 -5
- 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 +42 -143
- package/src/lib/composables/useAtomixGlassStyles.ts +61 -77
- package/src/lib/composables/usePerformanceMonitor.ts +5 -66
- package/src/lib/constants/components.ts +363 -46
- package/src/lib/types/components.ts +33 -1
- package/src/styles/01-settings/_settings.atomix-glass.scss +66 -28
- package/src/styles/02-tools/_tools.button.scss +51 -42
- package/src/styles/02-tools/_tools.glass.scss +45 -3
- package/src/styles/06-components/_components.atomix-glass.scss +116 -79
- package/src/components/AtomixGlass/PerformanceDashboard.tsx +0 -171
- package/src/components/AtomixGlass/animation-system.ts +0 -578
- package/src/components/AtomixGlass/deprecated/AtomixGlass.deprecated.tsx +0 -390
|
@@ -1,28 +1,20 @@
|
|
|
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;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Canonical 2D vector type shared across the glass math/shader utilities.
|
|
10
|
+
*
|
|
11
|
+
* Structurally identical to {@link MousePosition}; re-exported by `shader-utils`
|
|
12
|
+
* as the public `Vec2`.
|
|
10
13
|
*/
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
typeof pos1.x !== 'number' ||
|
|
16
|
-
typeof pos1.y !== 'number' ||
|
|
17
|
-
typeof pos2.x !== 'number' ||
|
|
18
|
-
typeof pos2.y !== 'number'
|
|
19
|
-
) {
|
|
20
|
-
return 0;
|
|
21
|
-
}
|
|
22
|
-
const deltaX = pos1.x - pos2.x;
|
|
23
|
-
const deltaY = pos1.y - pos2.y;
|
|
24
|
-
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
25
|
-
};
|
|
14
|
+
export interface Vec2 {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
}
|
|
26
18
|
|
|
27
19
|
/**
|
|
28
20
|
* Calculate element center from bounding rect
|
|
@@ -44,7 +36,7 @@ export const calculateMouseInfluence = (mouseOffset: MousePosition): number => {
|
|
|
44
36
|
if (!mouseOffset || typeof mouseOffset.x !== 'number' || typeof mouseOffset.y !== 'number') {
|
|
45
37
|
return 0;
|
|
46
38
|
}
|
|
47
|
-
//
|
|
39
|
+
// Clamp influence to keep mouse response subtle and stable.
|
|
48
40
|
const influence =
|
|
49
41
|
Math.sqrt(mouseOffset.x * mouseOffset.x + mouseOffset.y * mouseOffset.y) /
|
|
50
42
|
CONSTANTS.MOUSE_INFLUENCE_DIVISOR;
|
|
@@ -251,6 +243,79 @@ export const softClamp = (value: number, max: number): number => {
|
|
|
251
243
|
return max * (1 - Math.exp(-value / max));
|
|
252
244
|
};
|
|
253
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Clamp a value to the `[min, max]` range. Returns `min` for non-finite input.
|
|
248
|
+
*/
|
|
249
|
+
export const clamp = (value: number, min: number, max: number): number => {
|
|
250
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
251
|
+
return min;
|
|
252
|
+
}
|
|
253
|
+
return Math.max(min, Math.min(max, value));
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Two-edge smoothstep (GLSL semantics) — Hermite interpolation between `edge0`
|
|
258
|
+
* and `edge1`. Reuses {@link smoothstep} after normalizing `x` into `[0, 1]`.
|
|
259
|
+
*/
|
|
260
|
+
export const smoothstepEdge = (edge0: number, edge1: number, x: number): number => {
|
|
261
|
+
if (typeof edge0 !== 'number' || typeof edge1 !== 'number' || typeof x !== 'number') {
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
return smoothstep((x - edge0) / (edge1 - edge0));
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Cubic ease-in-out curve. `t` is clamped to `[0, 1]`.
|
|
269
|
+
*/
|
|
270
|
+
export const easeInOutCubic = (t: number): number => {
|
|
271
|
+
if (typeof t !== 'number' || isNaN(t)) {
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
275
|
+
return clamped < 0.5
|
|
276
|
+
? 4 * clamped * clamped * clamped
|
|
277
|
+
: 1 - Math.pow(-2 * clamped + 2, 3) / 2;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Quartic ease-out curve. `t` is clamped to `[0, 1]`.
|
|
282
|
+
*/
|
|
283
|
+
export const easeOutQuart = (t: number): number => {
|
|
284
|
+
if (typeof t !== 'number' || isNaN(t)) {
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
288
|
+
return 1 - Math.pow(1 - clamped, 4);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Overflow-safe Euclidean length of a 2D vector.
|
|
293
|
+
*/
|
|
294
|
+
export const vec2Length = (x: number, y: number): number => {
|
|
295
|
+
if (typeof x !== 'number' || typeof y !== 'number' || isNaN(x) || isNaN(y)) {
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
const maxComponent = Math.max(Math.abs(x), Math.abs(y));
|
|
299
|
+
if (maxComponent === 0) return 0;
|
|
300
|
+
const scaledX = x / maxComponent;
|
|
301
|
+
const scaledY = y / maxComponent;
|
|
302
|
+
return maxComponent * Math.sqrt(scaledX * scaledX + scaledY * scaledY);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Resolves the hover/active intensity multipliers from interaction state.
|
|
307
|
+
*
|
|
308
|
+
* Single source of truth for the `HOVER_INTENSITY` / `ACTIVE_INTENSITY`
|
|
309
|
+
* multipliers shared by the hook and the imperative style updater.
|
|
310
|
+
*/
|
|
311
|
+
export const getInteractionIntensity = (
|
|
312
|
+
isHovered: boolean,
|
|
313
|
+
isActive: boolean
|
|
314
|
+
): { hoverIntensity: number; activeIntensity: number } => ({
|
|
315
|
+
hoverIntensity: isHovered ? CONSTANTS.INTERACTION.HOVER_INTENSITY : 1,
|
|
316
|
+
activeIntensity: isActive ? CONSTANTS.INTERACTION.ACTIVE_INTENSITY : 1,
|
|
317
|
+
});
|
|
318
|
+
|
|
254
319
|
/**
|
|
255
320
|
* Spring-damper integration helper
|
|
256
321
|
* Calculates the next value based on velocity, stiffness, and damping.
|
|
@@ -280,6 +345,373 @@ export const calculateVelocity = (
|
|
|
280
345
|
return (current - previous) / deltaTime;
|
|
281
346
|
};
|
|
282
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Layout, sizing, and effect-resolution utilities for AtomixGlass.
|
|
350
|
+
*
|
|
351
|
+
* The root wrapper exposes CSS custom properties; the container owns layout and
|
|
352
|
+
* backdrop-filter. Helpers in this section keep decorative layers aligned with
|
|
353
|
+
* the container across fixed, sticky, and in-flow positioning modes.
|
|
354
|
+
*/
|
|
355
|
+
|
|
356
|
+
/** Subset of CSS layout properties used for glass positioning. */
|
|
357
|
+
export type GlassLayoutPosition = Pick<
|
|
358
|
+
CSSProperties,
|
|
359
|
+
'position' | 'top' | 'left' | 'right' | 'bottom' | 'inset'
|
|
360
|
+
>;
|
|
361
|
+
|
|
362
|
+
/** Inset values formatted for `--atomix-glass-*` custom properties. */
|
|
363
|
+
export interface GlassLayerInsetVars {
|
|
364
|
+
top: string;
|
|
365
|
+
left: string;
|
|
366
|
+
right: string;
|
|
367
|
+
bottom: string;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Normalizes a layout inset for use in CSS custom properties.
|
|
372
|
+
*
|
|
373
|
+
* @param value - Raw inset from `style` (number, px string, or `auto`).
|
|
374
|
+
* @param fallback - Value used when `value` is undefined.
|
|
375
|
+
*/
|
|
376
|
+
export function formatGlassInsetValue(
|
|
377
|
+
value: string | number | undefined,
|
|
378
|
+
fallback: string | number = 'auto'
|
|
379
|
+
): string {
|
|
380
|
+
if (value === undefined) {
|
|
381
|
+
return typeof fallback === 'number' ? `${fallback}px` : String(fallback);
|
|
382
|
+
}
|
|
383
|
+
if (value === 'auto') {
|
|
384
|
+
return 'auto';
|
|
385
|
+
}
|
|
386
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Determines whether the glass should use fixed/sticky layout semantics.
|
|
391
|
+
*
|
|
392
|
+
* @param explicit - Value of the `isFixedOrSticky` prop.
|
|
393
|
+
* @param position - `position` from the consumer `style` object.
|
|
394
|
+
*/
|
|
395
|
+
export function isGlassFixedOrSticky(
|
|
396
|
+
explicit?: boolean,
|
|
397
|
+
position?: CSSProperties['position']
|
|
398
|
+
): boolean {
|
|
399
|
+
return Boolean(explicit || position === 'fixed' || position === 'sticky');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Extracts layout-related properties from a React `CSSProperties` object.
|
|
404
|
+
*/
|
|
405
|
+
export function pickGlassLayoutStyle(style: CSSProperties): GlassLayoutPosition {
|
|
406
|
+
const { position, top, left, right, bottom, inset } = style;
|
|
407
|
+
return {
|
|
408
|
+
...(position != null && { position }),
|
|
409
|
+
...(top !== undefined && { top }),
|
|
410
|
+
...(left !== undefined && { left }),
|
|
411
|
+
...(right !== undefined && { right }),
|
|
412
|
+
...(bottom !== undefined && { bottom }),
|
|
413
|
+
...(inset !== undefined && { inset }),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Resolves inset custom properties for decorative layers (hover, borders, backgrounds).
|
|
419
|
+
*
|
|
420
|
+
* For fixed and sticky modes, insets mirror the container so sibling layers remain
|
|
421
|
+
* aligned. In-flow modes, insets follow the consumer `style` when a non-default
|
|
422
|
+
* `position` is provided.
|
|
423
|
+
*/
|
|
424
|
+
export function getGlassLayerInsetVars(
|
|
425
|
+
isFixedOrSticky: boolean,
|
|
426
|
+
restStyle: CSSProperties
|
|
427
|
+
): GlassLayerInsetVars {
|
|
428
|
+
if (isFixedOrSticky) {
|
|
429
|
+
return {
|
|
430
|
+
top: formatGlassInsetValue(restStyle.top, 0),
|
|
431
|
+
left: formatGlassInsetValue(restStyle.left, 0),
|
|
432
|
+
right: formatGlassInsetValue(restStyle.right, 'auto'),
|
|
433
|
+
bottom: formatGlassInsetValue(restStyle.bottom, 'auto'),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const position = restStyle.position;
|
|
438
|
+
const usesCustomPosition =
|
|
439
|
+
position != null && position !== 'static' && position !== 'relative';
|
|
440
|
+
|
|
441
|
+
if (!usesCustomPosition) {
|
|
442
|
+
return { top: '0px', left: '0px', right: 'auto', bottom: 'auto' };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
top: formatGlassInsetValue(restStyle.top, 0),
|
|
447
|
+
left: formatGlassInsetValue(restStyle.left, 0),
|
|
448
|
+
right: formatGlassInsetValue(restStyle.right, 'auto'),
|
|
449
|
+
bottom: formatGlassInsetValue(restStyle.bottom, 'auto'),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Resolves the `--atomix-glass-position` value for decorative layers.
|
|
455
|
+
*
|
|
456
|
+
* Fixed/sticky layers use the same positioning mode as the container; in-flow
|
|
457
|
+
* layers default to the internal absolute positioning context.
|
|
458
|
+
*/
|
|
459
|
+
export function getGlassLayerPositionVar(
|
|
460
|
+
isFixedOrSticky: boolean,
|
|
461
|
+
positionStyles: GlassLayoutPosition,
|
|
462
|
+
restStyle: CSSProperties
|
|
463
|
+
): string {
|
|
464
|
+
if (!isFixedOrSticky) {
|
|
465
|
+
return `${positionStyles.position}`;
|
|
466
|
+
}
|
|
467
|
+
const layout = pickGlassLayoutStyle(restStyle);
|
|
468
|
+
return `${layout.position ?? restStyle.position ?? 'fixed'}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Returns the internal positioning context for effect layers relative to the root.
|
|
473
|
+
*/
|
|
474
|
+
export function getGlassInternalPositionStyles(
|
|
475
|
+
isFixedOrSticky: boolean,
|
|
476
|
+
restStyle: CSSProperties
|
|
477
|
+
): GlassLayoutPosition {
|
|
478
|
+
return {
|
|
479
|
+
position: (isFixedOrSticky
|
|
480
|
+
? 'absolute'
|
|
481
|
+
: restStyle.position || 'absolute') as CSSProperties['position'],
|
|
482
|
+
top: 0,
|
|
483
|
+
left: 0,
|
|
484
|
+
right: 'auto',
|
|
485
|
+
bottom: 'auto',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Computes `--atomix-glass-width` and `--atomix-glass-height` values.
|
|
491
|
+
*
|
|
492
|
+
* Fixed/sticky elements prefer explicit dimensions or measured size; in-flow
|
|
493
|
+
* elements default to `100%`.
|
|
494
|
+
*/
|
|
495
|
+
export function resolveGlassAdjustedSize(options: {
|
|
496
|
+
width?: string | number;
|
|
497
|
+
height?: string | number;
|
|
498
|
+
restStyle: CSSProperties;
|
|
499
|
+
glassSize: GlassSize;
|
|
500
|
+
isFixedOrSticky: boolean;
|
|
501
|
+
}): { width: string; height: string } {
|
|
502
|
+
const { width, height, restStyle, glassSize, isFixedOrSticky } = options;
|
|
503
|
+
|
|
504
|
+
const resolveLength = (value: string | number | undefined, measured: number): string => {
|
|
505
|
+
if (value !== undefined && isFixedOrSticky) {
|
|
506
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
507
|
+
}
|
|
508
|
+
if (measured > 0 && isFixedOrSticky) {
|
|
509
|
+
return `${measured}px`;
|
|
510
|
+
}
|
|
511
|
+
return '100%';
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const effectiveWidth = width ?? restStyle.width;
|
|
515
|
+
const effectiveHeight = height ?? restStyle.height;
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
width: resolveLength(effectiveWidth, glassSize.width),
|
|
519
|
+
height: resolveLength(effectiveHeight, glassSize.height),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Input parameters for {@link buildGlassRootCssVariables}. */
|
|
524
|
+
export interface GlassRootCssVarsInput {
|
|
525
|
+
effectiveBorderRadius: number;
|
|
526
|
+
transformStyle?: string;
|
|
527
|
+
adjustedSize: { width: string; height: string };
|
|
528
|
+
isOverLight: boolean;
|
|
529
|
+
customZIndex?: string | number;
|
|
530
|
+
isFixedOrSticky: boolean;
|
|
531
|
+
positionStyles: GlassLayoutPosition;
|
|
532
|
+
restStyle: CSSProperties;
|
|
533
|
+
/** Rim width — maps to `--atomix-glass-border-width`. */
|
|
534
|
+
borderWidth?: string;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Builds the CSS custom properties applied to the root `.c-atomix-glass` element.
|
|
539
|
+
*
|
|
540
|
+
* These variables drive layer geometry, transforms, and stacking offsets. They
|
|
541
|
+
* must not include layout properties that would interfere with backdrop-filter.
|
|
542
|
+
*/
|
|
543
|
+
export function buildGlassRootCssVariables(input: GlassRootCssVarsInput): CSSProperties {
|
|
544
|
+
const {
|
|
545
|
+
effectiveBorderRadius,
|
|
546
|
+
transformStyle,
|
|
547
|
+
adjustedSize,
|
|
548
|
+
isOverLight,
|
|
549
|
+
customZIndex,
|
|
550
|
+
isFixedOrSticky,
|
|
551
|
+
positionStyles,
|
|
552
|
+
restStyle,
|
|
553
|
+
borderWidth = ATOMIX_GLASS.BORDER.DEFAULT_WIDTH,
|
|
554
|
+
} = input;
|
|
555
|
+
|
|
556
|
+
const layerPosition = getGlassLayerPositionVar(isFixedOrSticky, positionStyles, restStyle);
|
|
557
|
+
const layerInsets = getGlassLayerInsetVars(isFixedOrSticky, restStyle);
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
...(customZIndex !== undefined && { '--atomix-glass-base-z-index': customZIndex }),
|
|
561
|
+
'--atomix-glass-radius': `${effectiveBorderRadius}px`,
|
|
562
|
+
'--atomix-glass-transform': transformStyle || 'none',
|
|
563
|
+
'--atomix-glass-container-position': layerPosition,
|
|
564
|
+
'--atomix-glass-position': layerPosition,
|
|
565
|
+
'--atomix-glass-top': layerInsets.top,
|
|
566
|
+
'--atomix-glass-left': layerInsets.left,
|
|
567
|
+
'--atomix-glass-right': layerInsets.right,
|
|
568
|
+
'--atomix-glass-bottom': layerInsets.bottom,
|
|
569
|
+
'--atomix-glass-width': adjustedSize.width,
|
|
570
|
+
'--atomix-glass-height': adjustedSize.height,
|
|
571
|
+
// Aliases maintained for backward compatibility and consumer overrides.
|
|
572
|
+
'--atomix-glass-container-width': adjustedSize.width,
|
|
573
|
+
'--atomix-glass-container-height': adjustedSize.height,
|
|
574
|
+
[ATOMIX_GLASS.BORDER.WIDTH_CSS_VAR]: borderWidth,
|
|
575
|
+
'--atomix-glass-blend-mode': isOverLight ? 'multiply' : 'overlay',
|
|
576
|
+
} as CSSProperties;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Resolved visual effect values passed to {@link AtomixGlassContainer}. */
|
|
580
|
+
export interface ResolvedGlassContainerEffects {
|
|
581
|
+
displacementScale: number;
|
|
582
|
+
blurAmount: number;
|
|
583
|
+
saturation: number;
|
|
584
|
+
aberrationIntensity: number;
|
|
585
|
+
mouseOffset: MousePosition;
|
|
586
|
+
globalMousePosition: MousePosition;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Applies mode-specific multipliers and accessibility overrides to container effects.
|
|
591
|
+
*/
|
|
592
|
+
export function resolveGlassContainerEffects(options: {
|
|
593
|
+
displacementScale: number;
|
|
594
|
+
blurAmount: number;
|
|
595
|
+
saturation: number;
|
|
596
|
+
aberrationIntensity: number;
|
|
597
|
+
mode: DisplacementMode;
|
|
598
|
+
effectiveWithoutEffects: boolean;
|
|
599
|
+
effectiveHighContrast: boolean;
|
|
600
|
+
isOverLight: boolean;
|
|
601
|
+
saturationBoost: number;
|
|
602
|
+
mouseOffset: MousePosition;
|
|
603
|
+
globalMousePosition: MousePosition;
|
|
604
|
+
}): ResolvedGlassContainerEffects {
|
|
605
|
+
const { MULTIPLIERS, SATURATION } = ATOMIX_GLASS.CONSTANTS;
|
|
606
|
+
const zeroMouse: MousePosition = { x: 0, y: 0 };
|
|
607
|
+
|
|
608
|
+
const resolveSaturation = (): number => {
|
|
609
|
+
if (options.effectiveHighContrast) {
|
|
610
|
+
return SATURATION.HIGH_CONTRAST;
|
|
611
|
+
}
|
|
612
|
+
if (options.isOverLight) {
|
|
613
|
+
return options.saturation * options.saturationBoost;
|
|
614
|
+
}
|
|
615
|
+
return options.saturation;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
if (options.effectiveWithoutEffects) {
|
|
619
|
+
return {
|
|
620
|
+
displacementScale: 0,
|
|
621
|
+
blurAmount: 0,
|
|
622
|
+
saturation: resolveSaturation(),
|
|
623
|
+
aberrationIntensity: 0,
|
|
624
|
+
mouseOffset: zeroMouse,
|
|
625
|
+
globalMousePosition: zeroMouse,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let resolvedDisplacement = options.displacementScale;
|
|
630
|
+
if (options.mode === 'shader') {
|
|
631
|
+
resolvedDisplacement *= MULTIPLIERS.SHADER_DISPLACEMENT;
|
|
632
|
+
} else if (options.isOverLight) {
|
|
633
|
+
resolvedDisplacement *= MULTIPLIERS.OVER_LIGHT_DISPLACEMENT;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
let resolvedAberration = options.aberrationIntensity;
|
|
637
|
+
if (options.mode === 'shader') {
|
|
638
|
+
resolvedAberration *= MULTIPLIERS.SHADER_ABERRATION;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
displacementScale: resolvedDisplacement,
|
|
643
|
+
blurAmount: options.blurAmount,
|
|
644
|
+
saturation: resolveSaturation(),
|
|
645
|
+
aberrationIntensity: resolvedAberration,
|
|
646
|
+
mouseOffset: options.mouseOffset,
|
|
647
|
+
globalMousePosition: options.globalMousePosition,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Coerces a value to a finite number, returning `fallback` when invalid. */
|
|
652
|
+
export function toSafeNumber(value: unknown, fallback = 0): number {
|
|
653
|
+
return typeof value === 'number' && !isNaN(value) ? value : fallback;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export type DistortionQuality = 'low' | 'medium' | 'high' | 'ultra';
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Calculates the target frame rate for shader time-animation loops.
|
|
660
|
+
*
|
|
661
|
+
* Balances visual quality against distortion complexity and `animationSpeed`.
|
|
662
|
+
*/
|
|
663
|
+
export function getShaderAnimationTargetFps(options: {
|
|
664
|
+
distortionQuality: DistortionQuality | string;
|
|
665
|
+
animationSpeed?: number;
|
|
666
|
+
withMultiLayerDistortion?: boolean;
|
|
667
|
+
distortionOctaves?: number;
|
|
668
|
+
distortionLacunarity?: number;
|
|
669
|
+
distortionGain?: number;
|
|
670
|
+
}): number {
|
|
671
|
+
const {
|
|
672
|
+
distortionQuality,
|
|
673
|
+
animationSpeed = 1,
|
|
674
|
+
withMultiLayerDistortion,
|
|
675
|
+
distortionOctaves = 3,
|
|
676
|
+
distortionLacunarity = 2,
|
|
677
|
+
distortionGain = 0.5,
|
|
678
|
+
} = options;
|
|
679
|
+
|
|
680
|
+
const baseFps =
|
|
681
|
+
distortionQuality === 'ultra'
|
|
682
|
+
? 60
|
|
683
|
+
: distortionQuality === 'high'
|
|
684
|
+
? 30
|
|
685
|
+
: distortionQuality === 'medium'
|
|
686
|
+
? 24
|
|
687
|
+
: 20;
|
|
688
|
+
|
|
689
|
+
const effectiveSpeed = Math.max(0.5, Math.min(2, animationSpeed));
|
|
690
|
+
const complexity = withMultiLayerDistortion
|
|
691
|
+
? Math.max(
|
|
692
|
+
1,
|
|
693
|
+
distortionOctaves / 3 +
|
|
694
|
+
Math.max(0, distortionLacunarity - 2) * 0.25 +
|
|
695
|
+
Math.max(0, distortionGain - 0.5)
|
|
696
|
+
)
|
|
697
|
+
: 1;
|
|
698
|
+
|
|
699
|
+
return Math.max(12, Math.min(60, Math.round((baseFps * effectiveSpeed) / complexity)));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Computes per-channel displacement scale for the SVG chromatic-aberration filter.
|
|
704
|
+
*/
|
|
705
|
+
export function getChromaticDisplacementScale(
|
|
706
|
+
mode: DisplacementMode,
|
|
707
|
+
displacementScale: number,
|
|
708
|
+
aberrationIntensity: number,
|
|
709
|
+
channelFactor: number
|
|
710
|
+
): number {
|
|
711
|
+
const sign = mode === 'shader' ? 1 : -1;
|
|
712
|
+
return displacementScale * (sign - aberrationIntensity * channelFactor);
|
|
713
|
+
}
|
|
714
|
+
|
|
283
715
|
/**
|
|
284
716
|
* Get displacement map URL based on mode
|
|
285
717
|
*/
|
|
@@ -305,10 +737,12 @@ export const getDisplacementMap = (
|
|
|
305
737
|
}
|
|
306
738
|
};
|
|
307
739
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
740
|
+
/**
|
|
741
|
+
* Module-level LRU cache for shader displacement maps.
|
|
742
|
+
*
|
|
743
|
+
* Shared across all `AtomixGlassContainer` instances so identical size and
|
|
744
|
+
* variant combinations are generated once.
|
|
745
|
+
*/
|
|
312
746
|
export const MAX_CACHE_SIZE = 15;
|
|
313
747
|
|
|
314
748
|
export interface ShaderCacheEntry {
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// Adapted from https://github.com/shuding/liquid-glass
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import {
|
|
4
|
+
clamp,
|
|
5
|
+
easeInOutCubic,
|
|
6
|
+
easeOutQuart,
|
|
7
|
+
smoothstepEdge as smoothStep,
|
|
8
|
+
vec2Length as calculateLength,
|
|
9
|
+
} from './glass-utils';
|
|
10
|
+
import type { Vec2 } from './glass-utils';
|
|
11
|
+
|
|
12
|
+
export type { Vec2 } from './glass-utils';
|
|
7
13
|
|
|
8
14
|
export interface ShaderOptions {
|
|
9
15
|
width: number;
|
|
@@ -34,31 +40,6 @@ const DEFAULT_CANVAS_WIDTH = 256;
|
|
|
34
40
|
const DEFAULT_CANVAS_HEIGHT = 256;
|
|
35
41
|
|
|
36
42
|
// Utility functions
|
|
37
|
-
const smoothStep = (a: number, b: number, t: number): number => {
|
|
38
|
-
// Add input validation
|
|
39
|
-
if (typeof a !== 'number' || typeof b !== 'number' || typeof t !== 'number') {
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const clamped = Math.max(0, Math.min(1, (t - a) / (b - a)));
|
|
44
|
-
return clamped * clamped * (3 - 2 * clamped);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const calculateLength = (x: number, y: number): number => {
|
|
48
|
-
// Add input validation and error handling
|
|
49
|
-
if (typeof x !== 'number' || typeof y !== 'number' || isNaN(x) || isNaN(y)) {
|
|
50
|
-
return 0;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Prevent potential overflow
|
|
54
|
-
const maxX = Math.max(Math.abs(x), Math.abs(y));
|
|
55
|
-
if (maxX === 0) return 0;
|
|
56
|
-
|
|
57
|
-
const scaledX = x / maxX;
|
|
58
|
-
const scaledY = y / maxX;
|
|
59
|
-
return maxX * Math.sqrt(scaledX * scaledX + scaledY * scaledY);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
43
|
const roundedRectSDF = (
|
|
63
44
|
x: number,
|
|
64
45
|
y: number,
|
|
@@ -96,42 +77,6 @@ const validateVec2 = (vec: Vec2): boolean => {
|
|
|
96
77
|
);
|
|
97
78
|
};
|
|
98
79
|
|
|
99
|
-
const clampValue = (value: number, min: number, max: number): number => {
|
|
100
|
-
// Add input validation
|
|
101
|
-
if (typeof value !== 'number' || typeof min !== 'number' || typeof max !== 'number') {
|
|
102
|
-
return min;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (isNaN(value)) return min;
|
|
106
|
-
if (isNaN(min)) return 0;
|
|
107
|
-
if (isNaN(max)) return 1;
|
|
108
|
-
|
|
109
|
-
return Math.max(min, Math.min(max, value));
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Advanced easing functions for Apple-style smooth animations
|
|
113
|
-
const easeInOutCubic = (t: number): number => {
|
|
114
|
-
// Add input validation
|
|
115
|
-
if (typeof t !== 'number' || isNaN(t)) {
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const clampedT = Math.max(0, Math.min(1, t));
|
|
120
|
-
return clampedT < 0.5
|
|
121
|
-
? 4 * clampedT * clampedT * clampedT
|
|
122
|
-
: 1 - Math.pow(-2 * clampedT + 2, 3) / 2;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const easeOutQuart = (t: number): number => {
|
|
126
|
-
// Add input validation
|
|
127
|
-
if (typeof t !== 'number' || isNaN(t)) {
|
|
128
|
-
return 0;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const clampedT = Math.max(0, Math.min(1, t));
|
|
132
|
-
return 1 - Math.pow(1 - clampedT, 4);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
80
|
// Perlin-like noise for organic distortion
|
|
136
81
|
const noise2D = (x: number, y: number): number => {
|
|
137
82
|
// Add input validation
|
|
@@ -503,8 +448,8 @@ export const fragmentShaders = {
|
|
|
503
448
|
const finalY = iy + totalDistortionY + chromaticOffset.y * 0.5;
|
|
504
449
|
|
|
505
450
|
return createTexture(
|
|
506
|
-
|
|
507
|
-
|
|
451
|
+
clamp(finalX * scaled + 0.5, 0, 1),
|
|
452
|
+
clamp(finalY * scaled + 0.5, 0, 1)
|
|
508
453
|
);
|
|
509
454
|
},
|
|
510
455
|
|
|
@@ -545,7 +490,7 @@ export const fragmentShaders = {
|
|
|
545
490
|
const totalX = ix + (organicX * 0.035 + fluidVelocityX + vortexX) * mask;
|
|
546
491
|
const totalY = iy + (organicY * 0.035 + fluidVelocityY + vortexY) * mask;
|
|
547
492
|
|
|
548
|
-
return createTexture(
|
|
493
|
+
return createTexture(clamp(totalX + 0.5, 0, 1), clamp(totalY + 0.5, 0, 1));
|
|
549
494
|
},
|
|
550
495
|
|
|
551
496
|
// High-end glass with advanced refraction and depth
|
|
@@ -593,7 +538,7 @@ export const fragmentShaders = {
|
|
|
593
538
|
const finalX = ix + (refractionX + depthX + organicNoise * 0.015) * edgeMask;
|
|
594
539
|
const finalY = iy + (refractionY + depthY + organicNoise * 0.015) * edgeMask;
|
|
595
540
|
|
|
596
|
-
return createTexture(
|
|
541
|
+
return createTexture(clamp(finalX + 0.5, 0, 1), clamp(finalY + 0.5, 0, 1));
|
|
597
542
|
},
|
|
598
543
|
|
|
599
544
|
// Metallic liquid effect with shimmer
|
|
@@ -630,7 +575,7 @@ export const fragmentShaders = {
|
|
|
630
575
|
const totalX = ix + (wave1 + shimmer + Math.cos(flowAngle) * flowEffect) * mask;
|
|
631
576
|
const totalY = iy + (wave2 + shimmer * 0.8 + Math.sin(flowAngle) * flowEffect) * mask;
|
|
632
577
|
|
|
633
|
-
return createTexture(
|
|
578
|
+
return createTexture(clamp(totalX + 0.5, 0, 1), clamp(totalY + 0.5, 0, 1));
|
|
634
579
|
},
|
|
635
580
|
|
|
636
581
|
// basiBasi - Expert Premium Glass Shader
|
|
@@ -784,7 +729,7 @@ export const fragmentShaders = {
|
|
|
784
729
|
const finalX = ix + totalDistortionX * 0.85; // Scale down for subtlety
|
|
785
730
|
const finalY = iy + totalDistortionY * 0.85;
|
|
786
731
|
|
|
787
|
-
return createTexture(
|
|
732
|
+
return createTexture(clamp(finalX + 0.5, 0, 1), clamp(finalY + 0.5, 0, 1));
|
|
788
733
|
},
|
|
789
734
|
|
|
790
735
|
// Aliases for compatibility
|
|
@@ -903,13 +848,13 @@ export class ShaderDisplacementGenerator {
|
|
|
903
848
|
const g = smoothedDy / maxScale + 0.5;
|
|
904
849
|
|
|
905
850
|
const pixelIndex = (y * w + x) * 4;
|
|
906
|
-
data[pixelIndex] =
|
|
907
|
-
data[pixelIndex + 1] =
|
|
851
|
+
data[pixelIndex] = clamp(r * 255, NORMALIZATION_CLAMP.min, NORMALIZATION_CLAMP.max); // Red channel (X displacement)
|
|
852
|
+
data[pixelIndex + 1] = clamp(
|
|
908
853
|
g * 255,
|
|
909
854
|
NORMALIZATION_CLAMP.min,
|
|
910
855
|
NORMALIZATION_CLAMP.max
|
|
911
856
|
); // Green channel (Y displacement)
|
|
912
|
-
data[pixelIndex + 2] =
|
|
857
|
+
data[pixelIndex + 2] = clamp(
|
|
913
858
|
g * 255,
|
|
914
859
|
NORMALIZATION_CLAMP.min,
|
|
915
860
|
NORMALIZATION_CLAMP.max
|
|
@@ -951,6 +896,3 @@ export class ShaderDisplacementGenerator {
|
|
|
951
896
|
return this.canvasDPI;
|
|
952
897
|
}
|
|
953
898
|
}
|
|
954
|
-
|
|
955
|
-
// Re-export animation system functions for convenience
|
|
956
|
-
export { createFBMEngine, liquidGlassWithTime } from './animation-system';
|