@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.
- package/LICENSE +21 -0
- package/README.md +70 -2
- package/dist/chunk-A7JCTGZO.js +230 -0
- package/dist/chunk-A7JCTGZO.js.map +1 -0
- package/dist/core.cjs +238 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +77 -0
- package/dist/core.d.ts +77 -0
- package/dist/core.js +3 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +4288 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +1118 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +532 -0
- package/dist/index.d.ts +532 -0
- package/dist/index.js +4029 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -3
- package/src/components/animations/CountUp/CountUp.css +4 -0
- package/src/components/animations/CountUp/CountUp.tsx +100 -0
- package/src/components/animations/CountUp/index.ts +2 -0
- package/src/components/animations/FadeIn/FadeIn.css +43 -0
- package/src/components/animations/FadeIn/FadeIn.tsx +72 -0
- package/src/components/animations/FadeIn/index.ts +2 -0
- package/src/components/animations/Magnetic/Magnetic.css +11 -0
- package/src/components/animations/Magnetic/Magnetic.tsx +107 -0
- package/src/components/animations/Magnetic/index.ts +2 -0
- package/src/components/animations/ScaleIn/ScaleIn.css +24 -0
- package/src/components/animations/ScaleIn/ScaleIn.tsx +65 -0
- package/src/components/animations/ScaleIn/index.ts +2 -0
- package/src/components/animations/Spotlight/Spotlight.css +93 -0
- package/src/components/animations/Spotlight/Spotlight.tsx +100 -0
- package/src/components/animations/Spotlight/index.ts +2 -0
- package/src/components/animations/Tilt/Tilt.css +35 -0
- package/src/components/animations/Tilt/Tilt.tsx +123 -0
- package/src/components/animations/Tilt/index.ts +2 -0
- package/src/components/animations/index.ts +12 -0
- package/src/components/backgrounds/Aurora/Aurora.css +33 -0
- package/src/components/backgrounds/Aurora/Aurora.tsx +147 -0
- package/src/components/backgrounds/Aurora/index.ts +2 -0
- package/src/components/backgrounds/Caustics/Caustics.css +34 -0
- package/src/components/backgrounds/Caustics/Caustics.tsx +142 -0
- package/src/components/backgrounds/Caustics/index.ts +2 -0
- package/src/components/backgrounds/Contours/Contours.css +42 -0
- package/src/components/backgrounds/Contours/Contours.tsx +147 -0
- package/src/components/backgrounds/Contours/index.ts +2 -0
- package/src/components/backgrounds/DotField/DotField.css +44 -0
- package/src/components/backgrounds/DotField/DotField.tsx +135 -0
- package/src/components/backgrounds/DotField/index.ts +2 -0
- package/src/components/backgrounds/DragonScales/DragonScales.css +44 -0
- package/src/components/backgrounds/DragonScales/DragonScales.tsx +250 -0
- package/src/components/backgrounds/DragonScales/index.ts +2 -0
- package/src/components/backgrounds/Filaments/Filaments.css +38 -0
- package/src/components/backgrounds/Filaments/Filaments.tsx +138 -0
- package/src/components/backgrounds/Filaments/index.ts +2 -0
- package/src/components/backgrounds/FlipTiles/FlipTiles.css +44 -0
- package/src/components/backgrounds/FlipTiles/FlipTiles.tsx +368 -0
- package/src/components/backgrounds/FlipTiles/index.ts +2 -0
- package/src/components/backgrounds/Halo/Halo.css +43 -0
- package/src/components/backgrounds/Halo/Halo.tsx +122 -0
- package/src/components/backgrounds/Halo/index.ts +2 -0
- package/src/components/backgrounds/MeshGradient/MeshGradient.css +43 -0
- package/src/components/backgrounds/MeshGradient/MeshGradient.tsx +195 -0
- package/src/components/backgrounds/MeshGradient/index.ts +2 -0
- package/src/components/backgrounds/PixelVortex/PixelVortex.css +44 -0
- package/src/components/backgrounds/PixelVortex/PixelVortex.tsx +278 -0
- package/src/components/backgrounds/PixelVortex/index.ts +2 -0
- package/src/components/backgrounds/Planet/Planet.css +39 -0
- package/src/components/backgrounds/Planet/Planet.tsx +736 -0
- package/src/components/backgrounds/Planet/index.ts +2 -0
- package/src/components/backgrounds/SilkWaves/SilkWaves.css +44 -0
- package/src/components/backgrounds/SilkWaves/SilkWaves.tsx +144 -0
- package/src/components/backgrounds/SilkWaves/index.ts +2 -0
- package/src/components/backgrounds/Velvet/Velvet.css +35 -0
- package/src/components/backgrounds/Velvet/Velvet.tsx +246 -0
- package/src/components/backgrounds/Velvet/index.ts +2 -0
- package/src/components/backgrounds/index.ts +26 -0
- package/src/components/index.ts +4 -0
- package/src/components/media/AsciiImage/AsciiImage.css +16 -0
- package/src/components/media/AsciiImage/AsciiImage.tsx +366 -0
- package/src/components/media/AsciiImage/index.ts +2 -0
- package/src/components/media/index.ts +2 -0
- package/src/components/text/GradientText/GradientText.css +32 -0
- package/src/components/text/GradientText/GradientText.tsx +27 -0
- package/src/components/text/GradientText/index.ts +2 -0
- package/src/components/text/Marquee/Marquee.css +91 -0
- package/src/components/text/Marquee/Marquee.tsx +130 -0
- package/src/components/text/Marquee/index.ts +2 -0
- package/src/components/text/ParticleText/ParticleText.css +16 -0
- package/src/components/text/ParticleText/ParticleText.tsx +279 -0
- package/src/components/text/ParticleText/index.ts +2 -0
- package/src/components/text/RotatingText/RotatingText.css +133 -0
- package/src/components/text/RotatingText/RotatingText.tsx +112 -0
- package/src/components/text/RotatingText/index.ts +2 -0
- package/src/components/text/ScrambleText/ScrambleText.css +48 -0
- package/src/components/text/ScrambleText/ScrambleText.tsx +141 -0
- package/src/components/text/ScrambleText/index.ts +2 -0
- package/src/components/text/ScrollMarquee/ScrollMarquee.css +33 -0
- package/src/components/text/ScrollMarquee/ScrollMarquee.tsx +110 -0
- package/src/components/text/ScrollMarquee/index.ts +2 -0
- package/src/components/text/ScrollReveal/ScrollReveal.css +51 -0
- package/src/components/text/ScrollReveal/ScrollReveal.tsx +186 -0
- package/src/components/text/ScrollReveal/index.ts +2 -0
- package/src/components/text/SplitReveal/SplitReveal.css +52 -0
- package/src/components/text/SplitReveal/SplitReveal.tsx +127 -0
- package/src/components/text/SplitReveal/index.ts +2 -0
- package/src/components/text/Typewriter/Typewriter.css +37 -0
- package/src/components/text/Typewriter/Typewriter.tsx +80 -0
- package/src/components/text/Typewriter/index.ts +2 -0
- package/src/components/text/WaveText/WaveText.css +28 -0
- package/src/components/text/WaveText/WaveText.tsx +52 -0
- package/src/components/text/WaveText/index.ts +2 -0
- 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,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,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,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,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
|
+
}
|