@elyri/motion 0.0.0-stage → 0.0.1

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.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -2
  3. package/dist/chunk-A7JCTGZO.js +230 -0
  4. package/dist/chunk-A7JCTGZO.js.map +1 -0
  5. package/dist/core.cjs +238 -0
  6. package/dist/core.cjs.map +1 -0
  7. package/dist/core.d.cts +77 -0
  8. package/dist/core.d.ts +77 -0
  9. package/dist/core.js +3 -0
  10. package/dist/core.js.map +1 -0
  11. package/dist/index.cjs +4288 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.css +1118 -0
  14. package/dist/index.css.map +1 -0
  15. package/dist/index.d.cts +532 -0
  16. package/dist/index.d.ts +532 -0
  17. package/dist/index.js +4029 -0
  18. package/dist/index.js.map +1 -0
  19. package/package.json +60 -3
  20. package/src/components/animations/CountUp/CountUp.css +4 -0
  21. package/src/components/animations/CountUp/CountUp.tsx +100 -0
  22. package/src/components/animations/CountUp/index.ts +2 -0
  23. package/src/components/animations/FadeIn/FadeIn.css +43 -0
  24. package/src/components/animations/FadeIn/FadeIn.tsx +72 -0
  25. package/src/components/animations/FadeIn/index.ts +2 -0
  26. package/src/components/animations/Magnetic/Magnetic.css +11 -0
  27. package/src/components/animations/Magnetic/Magnetic.tsx +107 -0
  28. package/src/components/animations/Magnetic/index.ts +2 -0
  29. package/src/components/animations/ScaleIn/ScaleIn.css +24 -0
  30. package/src/components/animations/ScaleIn/ScaleIn.tsx +65 -0
  31. package/src/components/animations/ScaleIn/index.ts +2 -0
  32. package/src/components/animations/Spotlight/Spotlight.css +93 -0
  33. package/src/components/animations/Spotlight/Spotlight.tsx +100 -0
  34. package/src/components/animations/Spotlight/index.ts +2 -0
  35. package/src/components/animations/Tilt/Tilt.css +35 -0
  36. package/src/components/animations/Tilt/Tilt.tsx +123 -0
  37. package/src/components/animations/Tilt/index.ts +2 -0
  38. package/src/components/animations/index.ts +12 -0
  39. package/src/components/backgrounds/Aurora/Aurora.css +33 -0
  40. package/src/components/backgrounds/Aurora/Aurora.tsx +147 -0
  41. package/src/components/backgrounds/Aurora/index.ts +2 -0
  42. package/src/components/backgrounds/Caustics/Caustics.css +34 -0
  43. package/src/components/backgrounds/Caustics/Caustics.tsx +142 -0
  44. package/src/components/backgrounds/Caustics/index.ts +2 -0
  45. package/src/components/backgrounds/Contours/Contours.css +42 -0
  46. package/src/components/backgrounds/Contours/Contours.tsx +147 -0
  47. package/src/components/backgrounds/Contours/index.ts +2 -0
  48. package/src/components/backgrounds/DotField/DotField.css +44 -0
  49. package/src/components/backgrounds/DotField/DotField.tsx +135 -0
  50. package/src/components/backgrounds/DotField/index.ts +2 -0
  51. package/src/components/backgrounds/DragonScales/DragonScales.css +44 -0
  52. package/src/components/backgrounds/DragonScales/DragonScales.tsx +250 -0
  53. package/src/components/backgrounds/DragonScales/index.ts +2 -0
  54. package/src/components/backgrounds/Filaments/Filaments.css +38 -0
  55. package/src/components/backgrounds/Filaments/Filaments.tsx +138 -0
  56. package/src/components/backgrounds/Filaments/index.ts +2 -0
  57. package/src/components/backgrounds/FlipTiles/FlipTiles.css +44 -0
  58. package/src/components/backgrounds/FlipTiles/FlipTiles.tsx +368 -0
  59. package/src/components/backgrounds/FlipTiles/index.ts +2 -0
  60. package/src/components/backgrounds/Halo/Halo.css +43 -0
  61. package/src/components/backgrounds/Halo/Halo.tsx +122 -0
  62. package/src/components/backgrounds/Halo/index.ts +2 -0
  63. package/src/components/backgrounds/MeshGradient/MeshGradient.css +43 -0
  64. package/src/components/backgrounds/MeshGradient/MeshGradient.tsx +195 -0
  65. package/src/components/backgrounds/MeshGradient/index.ts +2 -0
  66. package/src/components/backgrounds/PixelVortex/PixelVortex.css +44 -0
  67. package/src/components/backgrounds/PixelVortex/PixelVortex.tsx +278 -0
  68. package/src/components/backgrounds/PixelVortex/index.ts +2 -0
  69. package/src/components/backgrounds/Planet/Planet.css +39 -0
  70. package/src/components/backgrounds/Planet/Planet.tsx +736 -0
  71. package/src/components/backgrounds/Planet/index.ts +2 -0
  72. package/src/components/backgrounds/SilkWaves/SilkWaves.css +44 -0
  73. package/src/components/backgrounds/SilkWaves/SilkWaves.tsx +144 -0
  74. package/src/components/backgrounds/SilkWaves/index.ts +2 -0
  75. package/src/components/backgrounds/Velvet/Velvet.css +35 -0
  76. package/src/components/backgrounds/Velvet/Velvet.tsx +246 -0
  77. package/src/components/backgrounds/Velvet/index.ts +2 -0
  78. package/src/components/backgrounds/index.ts +26 -0
  79. package/src/components/index.ts +4 -0
  80. package/src/components/media/AsciiImage/AsciiImage.css +16 -0
  81. package/src/components/media/AsciiImage/AsciiImage.tsx +366 -0
  82. package/src/components/media/AsciiImage/index.ts +2 -0
  83. package/src/components/media/index.ts +2 -0
  84. package/src/components/text/GradientText/GradientText.css +32 -0
  85. package/src/components/text/GradientText/GradientText.tsx +27 -0
  86. package/src/components/text/GradientText/index.ts +2 -0
  87. package/src/components/text/Marquee/Marquee.css +91 -0
  88. package/src/components/text/Marquee/Marquee.tsx +130 -0
  89. package/src/components/text/Marquee/index.ts +2 -0
  90. package/src/components/text/ParticleText/ParticleText.css +16 -0
  91. package/src/components/text/ParticleText/ParticleText.tsx +279 -0
  92. package/src/components/text/ParticleText/index.ts +2 -0
  93. package/src/components/text/RotatingText/RotatingText.css +133 -0
  94. package/src/components/text/RotatingText/RotatingText.tsx +112 -0
  95. package/src/components/text/RotatingText/index.ts +2 -0
  96. package/src/components/text/ScrambleText/ScrambleText.css +48 -0
  97. package/src/components/text/ScrambleText/ScrambleText.tsx +141 -0
  98. package/src/components/text/ScrambleText/index.ts +2 -0
  99. package/src/components/text/ScrollMarquee/ScrollMarquee.css +33 -0
  100. package/src/components/text/ScrollMarquee/ScrollMarquee.tsx +110 -0
  101. package/src/components/text/ScrollMarquee/index.ts +2 -0
  102. package/src/components/text/ScrollReveal/ScrollReveal.css +51 -0
  103. package/src/components/text/ScrollReveal/ScrollReveal.tsx +186 -0
  104. package/src/components/text/ScrollReveal/index.ts +2 -0
  105. package/src/components/text/SplitReveal/SplitReveal.css +52 -0
  106. package/src/components/text/SplitReveal/SplitReveal.tsx +127 -0
  107. package/src/components/text/SplitReveal/index.ts +2 -0
  108. package/src/components/text/Typewriter/Typewriter.css +37 -0
  109. package/src/components/text/Typewriter/Typewriter.tsx +80 -0
  110. package/src/components/text/Typewriter/index.ts +2 -0
  111. package/src/components/text/WaveText/WaveText.css +28 -0
  112. package/src/components/text/WaveText/WaveText.tsx +52 -0
  113. package/src/components/text/WaveText/index.ts +2 -0
  114. package/src/components/text/index.ts +20 -0
@@ -0,0 +1,100 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import type { CSSProperties, PointerEvent as ReactPointerEvent, ReactNode } from 'react';
3
+
4
+ import { cn } from '../../../core';
5
+
6
+ import './Spotlight.css';
7
+
8
+ export interface SpotlightProps {
9
+ children: ReactNode;
10
+ className?: string;
11
+ /** 光斑颜色 */
12
+ color?: string;
13
+ /** 光斑半径(像素) */
14
+ size?: number;
15
+ /** 是否显示卡片内部跟随指针的径向光晕 */
16
+ glow?: boolean;
17
+ /** 是否显示指针附近的边框光晕 */
18
+ borderGlow?: boolean;
19
+ /** 卡片圆角(像素) */
20
+ radius?: number;
21
+ }
22
+
23
+ /** 聚光卡片:指针位置泛起径向高光,可选择性地点亮边框;内部元素加 elyri-spotlight__item 可获得同款边框光晕 */
24
+ export function Spotlight({
25
+ children,
26
+ className,
27
+ color = '#7c8cff',
28
+ size = 240,
29
+ glow = true,
30
+ borderGlow = true,
31
+ radius = 16,
32
+ }: SpotlightProps) {
33
+ const elementRef = useRef<HTMLDivElement>(null);
34
+ const frameRef = useRef(0);
35
+ const pointerRef = useRef<{ x: number; y: number } | null>(null);
36
+ const itemsRef = useRef<HTMLElement[]>([]);
37
+
38
+ useEffect(
39
+ () => () => {
40
+ cancelAnimationFrame(frameRef.current);
41
+ },
42
+ [],
43
+ );
44
+
45
+ const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
46
+ pointerRef.current = { x: event.clientX, y: event.clientY };
47
+
48
+ if (frameRef.current) return;
49
+
50
+ frameRef.current = requestAnimationFrame(() => {
51
+ frameRef.current = 0;
52
+
53
+ const element = elementRef.current;
54
+ const pointer = pointerRef.current;
55
+ if (!element || !pointer) return;
56
+
57
+ const rect = element.getBoundingClientRect();
58
+ element.style.setProperty('--elyri-spotlight-x', `${pointer.x - rect.left}px`);
59
+ element.style.setProperty('--elyri-spotlight-y', `${pointer.y - rect.top}px`);
60
+
61
+ // 内部组件以自己的边框呈现光晕,需要相对自身的指针坐标
62
+ for (const item of itemsRef.current) {
63
+ const itemRect = item.getBoundingClientRect();
64
+ item.style.setProperty('--elyri-spotlight-x', `${pointer.x - itemRect.left}px`);
65
+ item.style.setProperty('--elyri-spotlight-y', `${pointer.y - itemRect.top}px`);
66
+ }
67
+ });
68
+ };
69
+
70
+ const handlePointerEnter = (event: ReactPointerEvent<HTMLDivElement>) => {
71
+ // 悬停期间缓存内部光晕项,避免每帧查询 DOM
72
+ itemsRef.current = Array.from(
73
+ event.currentTarget.querySelectorAll<HTMLElement>('.elyri-spotlight__item'),
74
+ );
75
+ handlePointerMove(event);
76
+ };
77
+
78
+ const style = {
79
+ '--elyri-spotlight-color': color,
80
+ '--elyri-spotlight-size': `${size}px`,
81
+ '--elyri-spotlight-radius': `${radius}px`,
82
+ } as CSSProperties;
83
+
84
+ return (
85
+ <div
86
+ ref={elementRef}
87
+ className={cn(
88
+ 'elyri-spotlight',
89
+ glow && 'elyri-spotlight--glow',
90
+ borderGlow && 'elyri-spotlight--border',
91
+ className,
92
+ )}
93
+ style={style}
94
+ onPointerEnter={handlePointerEnter}
95
+ onPointerMove={handlePointerMove}
96
+ >
97
+ <div className="elyri-spotlight__content">{children}</div>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,2 @@
1
+ export { Spotlight } from './Spotlight';
2
+ export type { SpotlightProps } from './Spotlight';
@@ -0,0 +1,35 @@
1
+ .elyri-tilt {
2
+ position: relative;
3
+ transform: perspective(var(--elyri-tilt-perspective, 800px)) rotateX(var(--elyri-tilt-rotate-x, 0deg))
4
+ rotateY(var(--elyri-tilt-rotate-y, 0deg));
5
+ transform-style: preserve-3d;
6
+ will-change: transform;
7
+ }
8
+
9
+ .elyri-tilt__glare {
10
+ position: absolute;
11
+ inset: 0;
12
+ border-radius: inherit;
13
+ background: radial-gradient(
14
+ circle at var(--elyri-tilt-glare-x, 50%) var(--elyri-tilt-glare-y, 50%),
15
+ rgba(255, 255, 255, 0.4),
16
+ transparent 55%
17
+ );
18
+ opacity: 0;
19
+ transition: opacity var(--elyri-duration-base) var(--elyri-ease-out);
20
+ pointer-events: none;
21
+ }
22
+
23
+ .elyri-tilt--active .elyri-tilt__glare {
24
+ opacity: 1;
25
+ }
26
+
27
+ @media (prefers-reduced-motion: reduce) {
28
+ .elyri-tilt {
29
+ transform: none;
30
+ }
31
+
32
+ .elyri-tilt__glare {
33
+ display: none;
34
+ }
35
+ }
@@ -0,0 +1,123 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { CSSProperties, PointerEvent as ReactPointerEvent, ReactNode } from 'react';
3
+
4
+ import { cn, usePrefersReducedMotion } from '../../../core';
5
+
6
+ import './Tilt.css';
7
+
8
+ /** 指针离开后回正所用的时长(毫秒) */
9
+ const RESET_DURATION = 400;
10
+
11
+ const easeOutCubic = (progress: number) => 1 - (1 - progress) ** 3;
12
+
13
+ export interface TiltProps {
14
+ children: ReactNode;
15
+ className?: string;
16
+ /** 最大倾斜角度(度) */
17
+ max?: number;
18
+ /** 透视距离(像素) */
19
+ perspective?: number;
20
+ /** 是否显示跟随指针的高光 */
21
+ glare?: boolean;
22
+ }
23
+
24
+ /** 指针跟随倾斜:根据指针在元素内的位置计算 3D 旋转 */
25
+ export function Tilt({ children, className, max = 10, perspective = 800, glare = true }: TiltProps) {
26
+ const elementRef = useRef<HTMLDivElement>(null);
27
+ const moveFrameRef = useRef(0);
28
+ const resetFrameRef = useRef(0);
29
+ const rotationRef = useRef({ x: 0, y: 0 });
30
+ const pointerRef = useRef<{ x: number; y: number } | null>(null);
31
+ const prefersReducedMotion = usePrefersReducedMotion();
32
+ const [isActive, setIsActive] = useState(false);
33
+
34
+ useEffect(
35
+ () => () => {
36
+ cancelAnimationFrame(moveFrameRef.current);
37
+ cancelAnimationFrame(resetFrameRef.current);
38
+ },
39
+ [],
40
+ );
41
+
42
+ const setRotation = (x: number, y: number) => {
43
+ rotationRef.current = { x, y };
44
+
45
+ const element = elementRef.current;
46
+ if (!element) return;
47
+
48
+ element.style.setProperty('--elyri-tilt-rotate-x', `${x}deg`);
49
+ element.style.setProperty('--elyri-tilt-rotate-y', `${y}deg`);
50
+ };
51
+
52
+ const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
53
+ if (prefersReducedMotion) return;
54
+
55
+ // 指针重新进入时打断尚未结束的回正
56
+ cancelAnimationFrame(resetFrameRef.current);
57
+ resetFrameRef.current = 0;
58
+
59
+ pointerRef.current = { x: event.clientX, y: event.clientY };
60
+ setIsActive(true);
61
+
62
+ if (moveFrameRef.current) return;
63
+
64
+ moveFrameRef.current = requestAnimationFrame(() => {
65
+ moveFrameRef.current = 0;
66
+
67
+ const element = elementRef.current;
68
+ const pointer = pointerRef.current;
69
+ if (!element || !pointer) return;
70
+
71
+ const rect = element.getBoundingClientRect();
72
+ if (!rect.width || !rect.height) return;
73
+
74
+ const ratioX = (pointer.x - rect.left) / rect.width;
75
+ const ratioY = (pointer.y - rect.top) / rect.height;
76
+
77
+ setRotation((0.5 - ratioY) * 2 * max, (ratioX - 0.5) * 2 * max);
78
+ element.style.setProperty('--elyri-tilt-glare-x', `${ratioX * 100}%`);
79
+ element.style.setProperty('--elyri-tilt-glare-y', `${ratioY * 100}%`);
80
+ });
81
+ };
82
+
83
+ const handlePointerEnd = () => {
84
+ cancelAnimationFrame(moveFrameRef.current);
85
+ moveFrameRef.current = 0;
86
+ cancelAnimationFrame(resetFrameRef.current);
87
+ resetFrameRef.current = 0;
88
+
89
+ pointerRef.current = null;
90
+ setIsActive(false);
91
+
92
+ const from = { ...rotationRef.current };
93
+ if (prefersReducedMotion || (!from.x && !from.y)) return;
94
+
95
+ // 缓动补间回正,避免指针离开时角度瞬间归零
96
+ const startedAt = performance.now();
97
+
98
+ const step = (now: number) => {
99
+ const progress = Math.min((now - startedAt) / RESET_DURATION, 1);
100
+ const remaining = 1 - easeOutCubic(progress);
101
+
102
+ setRotation(from.x * remaining, from.y * remaining);
103
+
104
+ if (progress < 1) resetFrameRef.current = requestAnimationFrame(step);
105
+ };
106
+
107
+ resetFrameRef.current = requestAnimationFrame(step);
108
+ };
109
+
110
+ return (
111
+ <div
112
+ ref={elementRef}
113
+ className={cn('elyri-tilt', isActive && 'elyri-tilt--active', className)}
114
+ style={{ '--elyri-tilt-perspective': `${perspective}px` } as CSSProperties}
115
+ onPointerMove={handlePointerMove}
116
+ onPointerLeave={handlePointerEnd}
117
+ onPointerCancel={handlePointerEnd}
118
+ >
119
+ {children}
120
+ {glare && <span className="elyri-tilt__glare" aria-hidden="true" />}
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,2 @@
1
+ export { Tilt } from './Tilt';
2
+ export type { TiltProps } from './Tilt';
@@ -0,0 +1,12 @@
1
+ export { CountUp } from './CountUp';
2
+ export type { CountUpProps } from './CountUp';
3
+ export { FadeIn } from './FadeIn';
4
+ export type { FadeInDirection, FadeInProps } from './FadeIn';
5
+ export { Magnetic } from './Magnetic';
6
+ export type { MagneticProps } from './Magnetic';
7
+ export { ScaleIn } from './ScaleIn';
8
+ export type { ScaleInProps } from './ScaleIn';
9
+ export { Spotlight } from './Spotlight';
10
+ export type { SpotlightProps } from './Spotlight';
11
+ export { Tilt } from './Tilt';
12
+ export type { TiltProps } from './Tilt';
@@ -0,0 +1,33 @@
1
+ .elyri-aurora {
2
+ --elyri-aurora-color: #3cffb0;
3
+
4
+ position: relative;
5
+ isolation: isolate;
6
+ overflow: hidden;
7
+ /* WebGL 不可用或尚未绘制时的底色 */
8
+ background:
9
+ radial-gradient(90% 60% at 50% 55%, color-mix(in srgb, var(--elyri-aurora-color) 14%, transparent), transparent),
10
+ linear-gradient(#020409, #05091a);
11
+ }
12
+
13
+ .elyri-aurora__canvas {
14
+ position: absolute;
15
+ inset: 0;
16
+ z-index: 0;
17
+ display: block;
18
+ width: 100%;
19
+ height: 100%;
20
+ opacity: 0;
21
+ pointer-events: none;
22
+ transition: opacity var(--elyri-duration-slow) var(--elyri-ease-out);
23
+ }
24
+
25
+ .elyri-aurora__canvas[data-ready] {
26
+ opacity: 1;
27
+ }
28
+
29
+ .elyri-aurora__content {
30
+ position: relative;
31
+ z-index: 1;
32
+ height: 100%;
33
+ }
@@ -0,0 +1,147 @@
1
+ import type { CSSProperties, ReactNode } from 'react';
2
+
3
+ import { cn, hexToLinearRgb, useShaderCanvas } from '../../../core';
4
+
5
+ import './Aurora.css';
6
+
7
+ export interface AuroraProps {
8
+ children?: ReactNode;
9
+ className?: string;
10
+ /** 三段渐变色:帘底、中段、帘顶(hex) */
11
+ colors?: [string, string, string];
12
+ /** 动画速度倍率 */
13
+ speed?: number;
14
+ /** 极光亮度倍率 */
15
+ intensity?: number;
16
+ /** 是否显示星空 */
17
+ stars?: boolean;
18
+ /** 指针附近的极光是否增亮 */
19
+ interactive?: boolean;
20
+ }
21
+
22
+ const DEFAULT_COLORS: [string, string, string] = ['#3cffb0', '#2a8cff', '#c04dff'];
23
+
24
+ // 多层极光帘幕:每层是一条被 fbm 扭动的曲线,下缘锐利、向上柔和衰减,
25
+ // 叠加沿水平方向的高频竖向光束,颜色随高度从帘底色过渡到帘顶色。
26
+ const FRAGMENT_SHADER = `
27
+ precision highp float;
28
+
29
+ uniform vec2 uResolution;
30
+ uniform float uTime;
31
+ uniform vec2 uPointer;
32
+ uniform vec3 uColor1;
33
+ uniform vec3 uColor2;
34
+ uniform vec3 uColor3;
35
+ uniform float uIntensity;
36
+ uniform float uStars;
37
+
38
+ float hash(vec2 p) {
39
+ p = fract(p * vec2(123.34, 456.21));
40
+ p += dot(p, p + 45.32);
41
+ return fract(p.x * p.y);
42
+ }
43
+
44
+ float noise(vec2 p) {
45
+ vec2 i = floor(p);
46
+ vec2 f = fract(p);
47
+ vec2 u = f * f * (3.0 - 2.0 * f);
48
+ return mix(
49
+ mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),
50
+ mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),
51
+ u.y
52
+ );
53
+ }
54
+
55
+ float fbm(vec2 p) {
56
+ float v = 0.0;
57
+ float a = 0.5;
58
+ for (int i = 0; i < 4; i++) {
59
+ v += a * noise(p);
60
+ p = p * 2.03 + vec2(17.1, 9.2);
61
+ a *= 0.5;
62
+ }
63
+ return v;
64
+ }
65
+
66
+ vec3 aces(vec3 x) {
67
+ return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
68
+ }
69
+
70
+ void main() {
71
+ vec2 uv = gl_FragCoord.xy / uResolution.y;
72
+ vec2 pointer = vec2(uPointer.x, uResolution.y - uPointer.y) / uResolution.y;
73
+ float t = uTime;
74
+
75
+ // 夜空底色:地平线附近略带帘底色的微光
76
+ vec3 color = mix(vec3(0.002, 0.004, 0.012), vec3(0.006, 0.01, 0.03), uv.y);
77
+ color += uColor1 * 0.025 * exp(-uv.y * 5.0);
78
+
79
+ // 星空
80
+ vec2 grid = uv * 55.0;
81
+ vec2 cell = floor(grid);
82
+ float rnd = hash(cell);
83
+ vec2 offset = vec2(hash(cell + 3.1), hash(cell + 7.7)) - 0.5;
84
+ float starDist = length(fract(grid) - 0.5 - offset * 0.7);
85
+ float twinkle = 0.6 + 0.4 * sin(t * (1.5 + rnd * 3.0) + rnd * 40.0);
86
+ float star = step(0.93, rnd) * exp(-starDist * starDist * 180.0) * twinkle * smoothstep(0.1, 0.6, uv.y);
87
+ color += vec3(0.8, 0.85, 1.0) * star * uStars;
88
+
89
+ vec3 aurora = vec3(0.0);
90
+ for (int i = 0; i < 4; i++) {
91
+ float fi = float(i);
92
+ float x = uv.x * (0.6 + fi * 0.15) + fi * 3.7;
93
+ float curve = 0.42 + fi * 0.09 + (fbm(vec2(x * 1.2 - t * 0.05 * (1.0 + fi * 0.3), fi * 5.3 + t * 0.03)) - 0.5) * 0.45;
94
+ float d = uv.y - curve;
95
+ float body = d > 0.0 ? exp(-d * (3.5 + fi)) : exp(d * 40.0);
96
+ float rays = fbm(vec2(x * 28.0 + curve * 6.0, t * 0.35 + fi));
97
+ rays = 0.25 + 1.5 * rays * rays;
98
+ float flow = 0.6 + 0.4 * sin(x * 3.0 - t * 0.4 + fi * 1.7);
99
+ vec3 c = mix(uColor1, uColor2, smoothstep(0.0, 0.3, d));
100
+ c = mix(c, uColor3, smoothstep(0.12, 0.55, d) * (0.55 + 0.45 * sin(fi * 2.1 + x * 0.8 + t * 0.1)));
101
+ aurora += c * body * rays * flow * (0.5 - fi * 0.08);
102
+ }
103
+
104
+ vec2 dp = uv - pointer;
105
+ aurora *= 1.0 + 0.7 * exp(-dot(dp, dp) * 6.0);
106
+ color += aurora * uIntensity;
107
+
108
+ vec2 vc = gl_FragCoord.xy / uResolution - 0.5;
109
+ color *= 1.0 - dot(vc, vc) * 0.9;
110
+ color = pow(aces(color * 1.2), vec3(1.0 / 2.2));
111
+ color += (hash(gl_FragCoord.xy + fract(t)) - 0.5) / 255.0;
112
+
113
+ gl_FragColor = vec4(color, 1.0);
114
+ }
115
+ `;
116
+
117
+ export function Aurora({
118
+ children,
119
+ className,
120
+ colors = DEFAULT_COLORS,
121
+ speed = 1,
122
+ intensity = 1,
123
+ stars = true,
124
+ interactive = true,
125
+ }: AuroraProps) {
126
+ const [bottom = DEFAULT_COLORS[0], middle = DEFAULT_COLORS[1], top = DEFAULT_COLORS[2]] = colors;
127
+
128
+ const canvasRef = useShaderCanvas({
129
+ fragmentShader: FRAGMENT_SHADER,
130
+ speed,
131
+ interactive,
132
+ onDraw: ({ gl, uniform }) => {
133
+ gl.uniform3fv(uniform('uColor1'), hexToLinearRgb(bottom));
134
+ gl.uniform3fv(uniform('uColor2'), hexToLinearRgb(middle));
135
+ gl.uniform3fv(uniform('uColor3'), hexToLinearRgb(top));
136
+ gl.uniform1f(uniform('uIntensity'), Math.max(0, intensity));
137
+ gl.uniform1f(uniform('uStars'), stars ? 1 : 0);
138
+ },
139
+ });
140
+
141
+ return (
142
+ <div className={cn('elyri-aurora', className)} style={{ '--elyri-aurora-color': bottom } as CSSProperties}>
143
+ <canvas ref={canvasRef} className="elyri-aurora__canvas" aria-hidden="true" />
144
+ {children != null && <div className="elyri-aurora__content">{children}</div>}
145
+ </div>
146
+ );
147
+ }
@@ -0,0 +1,2 @@
1
+ export { Aurora } from './Aurora';
2
+ export type { AuroraProps } from './Aurora';
@@ -0,0 +1,34 @@
1
+ .elyri-caustics {
2
+ --elyri-caustics-color: #0a6fa8;
3
+
4
+ position: relative;
5
+ isolation: isolate;
6
+ overflow: hidden;
7
+ /* WebGL 不可用或尚未绘制时的底色 */
8
+ background: linear-gradient(
9
+ color-mix(in srgb, var(--elyri-caustics-color) 60%, #000),
10
+ color-mix(in srgb, var(--elyri-caustics-color) 12%, #000)
11
+ );
12
+ }
13
+
14
+ .elyri-caustics__canvas {
15
+ position: absolute;
16
+ inset: 0;
17
+ z-index: 0;
18
+ display: block;
19
+ width: 100%;
20
+ height: 100%;
21
+ opacity: 0;
22
+ pointer-events: none;
23
+ transition: opacity var(--elyri-duration-slow) var(--elyri-ease-out);
24
+ }
25
+
26
+ .elyri-caustics__canvas[data-ready] {
27
+ opacity: 1;
28
+ }
29
+
30
+ .elyri-caustics__content {
31
+ position: relative;
32
+ z-index: 1;
33
+ height: 100%;
34
+ }
@@ -0,0 +1,142 @@
1
+ import type { CSSProperties, ReactNode } from 'react';
2
+
3
+ import { cn, hexToLinearRgb, useShaderCanvas } from '../../../core';
4
+
5
+ import './Caustics.css';
6
+
7
+ export interface CausticsProps {
8
+ children?: ReactNode;
9
+ className?: string;
10
+ /** 水体颜色(hex) */
11
+ color?: string;
12
+ /** 焦散光色(hex) */
13
+ highlightColor?: string;
14
+ /** 光纹密度倍率 */
15
+ density?: number;
16
+ /** 动画速度倍率 */
17
+ speed?: number;
18
+ /** 指针处是否荡开水波并聚光 */
19
+ interactive?: boolean;
20
+ }
21
+
22
+ // 水下焦散:两层运动 Voronoi 的细胞边缘构成光网,交汇处叠加增亮;
23
+ // RGB 三通道轻微错位模拟色散,再加顶部斜射的光束与随深度变暗的水体。
24
+ const FRAGMENT_SHADER = `
25
+ precision highp float;
26
+
27
+ uniform vec2 uResolution;
28
+ uniform float uTime;
29
+ uniform vec2 uPointer;
30
+ uniform vec3 uBase;
31
+ uniform vec3 uHighlight;
32
+ uniform float uDensity;
33
+ uniform float uRipple;
34
+
35
+ float hash(vec2 p) {
36
+ p = fract(p * vec2(123.34, 456.21));
37
+ p += dot(p, p + 45.32);
38
+ return fract(p.x * p.y);
39
+ }
40
+
41
+ vec2 hash2(vec2 p) {
42
+ p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
43
+ return fract(sin(p) * 43758.5453);
44
+ }
45
+
46
+ float cellEdge(vec2 p, float t) {
47
+ vec2 i = floor(p);
48
+ vec2 f = fract(p);
49
+ float f1 = 8.0;
50
+ float f2 = 8.0;
51
+ for (int y = -1; y <= 1; y++) {
52
+ for (int x = -1; x <= 1; x++) {
53
+ vec2 g = vec2(float(x), float(y));
54
+ vec2 o = 0.5 + 0.4 * sin(t * 0.8 + 6.2831 * hash2(i + g));
55
+ float d = length(g + o - f);
56
+ if (d < f1) {
57
+ f2 = f1;
58
+ f1 = d;
59
+ } else if (d < f2) {
60
+ f2 = d;
61
+ }
62
+ }
63
+ }
64
+ return f2 - f1;
65
+ }
66
+
67
+ float caustic(vec2 p, float t) {
68
+ p += 0.25 * vec2(sin(p.y * 1.3 + t * 0.7), cos(p.x * 1.1 - t * 0.6));
69
+ float a = exp(-cellEdge(p, t) * 9.0);
70
+ float b = exp(-cellEdge(p * 1.7 + vec2(3.1, 7.4), t * 1.3) * 9.0);
71
+ return a * 0.7 + b * 0.45 + a * b * 1.5;
72
+ }
73
+
74
+ vec3 aces(vec3 x) {
75
+ return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
76
+ }
77
+
78
+ void main() {
79
+ vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y);
80
+ float minRes = min(uResolution.x, uResolution.y);
81
+ float t = uTime;
82
+ vec2 p = frag / minRes * 4.0 * uDensity;
83
+
84
+ // 指针处荡开的水波
85
+ vec2 dp = (frag - uPointer) / minRes;
86
+ float r = length(dp);
87
+ p += dp / (r + 1e-4) * sin(r * 38.0 - t * 4.0) * 0.06 * exp(-r * 5.0) * uRipple;
88
+
89
+ vec3 caus = vec3(
90
+ caustic(p + vec2(0.014, 0.0), t),
91
+ caustic(p, t),
92
+ caustic(p - vec2(0.014, 0.0), t)
93
+ );
94
+
95
+ float depth = frag.y / uResolution.y;
96
+ vec3 color = mix(uBase * 0.35, uBase * 0.025, depth);
97
+
98
+ // 斜射光束
99
+ float x = frag.x / minRes + depth * 0.35;
100
+ float rays = (0.5 + 0.5 * sin(x * 9.0 + sin(x * 3.1 + t * 0.3) * 2.0 + t * 0.2)) * (0.5 + 0.5 * sin(x * 17.0 - t * 0.35));
101
+ color += uHighlight * 0.12 * rays * rays * (1.0 - depth) * (1.0 - depth);
102
+
103
+ float focus = 0.6 + 0.8 * exp(-dot(dp, dp) * 3.0) * uRipple;
104
+ color += uHighlight * caus * 0.5 * mix(1.0, 0.3, depth) * focus;
105
+
106
+ vec2 vc = frag / uResolution - 0.5;
107
+ color *= 1.0 - dot(vc, vc) * 1.1;
108
+ color = pow(aces(color * 1.2), vec3(1.0 / 2.2));
109
+ color += (hash(frag + fract(t)) - 0.5) / 255.0;
110
+
111
+ gl_FragColor = vec4(color, 1.0);
112
+ }
113
+ `;
114
+
115
+ export function Caustics({
116
+ children,
117
+ className,
118
+ color = '#0a6fa8',
119
+ highlightColor = '#c8fbff',
120
+ density = 1,
121
+ speed = 1,
122
+ interactive = true,
123
+ }: CausticsProps) {
124
+ const canvasRef = useShaderCanvas({
125
+ fragmentShader: FRAGMENT_SHADER,
126
+ speed,
127
+ interactive,
128
+ onDraw: ({ gl, uniform }) => {
129
+ gl.uniform3fv(uniform('uBase'), hexToLinearRgb(color));
130
+ gl.uniform3fv(uniform('uHighlight'), hexToLinearRgb(highlightColor));
131
+ gl.uniform1f(uniform('uDensity'), Math.max(0.2, density));
132
+ gl.uniform1f(uniform('uRipple'), interactive ? 1 : 0);
133
+ },
134
+ });
135
+
136
+ return (
137
+ <div className={cn('elyri-caustics', className)} style={{ '--elyri-caustics-color': color } as CSSProperties}>
138
+ <canvas ref={canvasRef} className="elyri-caustics__canvas" aria-hidden="true" />
139
+ {children != null && <div className="elyri-caustics__content">{children}</div>}
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,2 @@
1
+ export { Caustics } from './Caustics';
2
+ export type { CausticsProps } from './Caustics';
@@ -0,0 +1,42 @@
1
+ .elyri-contours {
2
+ --elyri-contours-color: #8a90a6;
3
+ --elyri-contours-bg: #0a0b0f;
4
+
5
+ position: relative;
6
+ isolation: isolate;
7
+ overflow: hidden;
8
+ /* WebGL 不可用或尚未绘制时的底色:同心细环近似等高线 */
9
+ background:
10
+ repeating-radial-gradient(
11
+ circle at 62% 38%,
12
+ transparent 0 26px,
13
+ color-mix(in srgb, var(--elyri-contours-color) 30%, transparent) 26px 27px
14
+ ),
15
+ var(--elyri-contours-bg);
16
+ }
17
+
18
+ .elyri-contours[data-scheme='light'] {
19
+ --elyri-contours-bg: #f9f9f7;
20
+ }
21
+
22
+ .elyri-contours__canvas {
23
+ position: absolute;
24
+ inset: 0;
25
+ z-index: 0;
26
+ display: block;
27
+ width: 100%;
28
+ height: 100%;
29
+ opacity: 0;
30
+ pointer-events: none;
31
+ transition: opacity var(--elyri-duration-slow) var(--elyri-ease-out);
32
+ }
33
+
34
+ .elyri-contours__canvas[data-ready] {
35
+ opacity: 1;
36
+ }
37
+
38
+ .elyri-contours__content {
39
+ position: relative;
40
+ z-index: 1;
41
+ height: 100%;
42
+ }