@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,147 @@
1
+ import type { CSSProperties, ReactNode } from 'react';
2
+
3
+ import { cn, hexToLinearRgb, useColorScheme, useShaderCanvas } from '../../../core';
4
+
5
+ import './Contours.css';
6
+
7
+ export interface ContoursProps {
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
+ // 等高线:域扭曲的分形噪声作为缓慢演化的地形,按固定高程间隔取等值线。
23
+ // 用有限差分求像素级梯度,把「到最近等值线的高程差」换算成像素距离,
24
+ // 从而得到处处等宽、抗锯齿的细线;每五条加粗一条计曲线,线过密处自动淡出防摩尔纹。
25
+ const FRAGMENT_SHADER = `
26
+ precision highp float;
27
+
28
+ uniform vec2 uResolution;
29
+ uniform float uTime;
30
+ uniform vec2 uPointer;
31
+ uniform vec3 uLine;
32
+ uniform vec3 uHighlight;
33
+ uniform vec3 uBg;
34
+ uniform float uLevels;
35
+ uniform float uDpr;
36
+
37
+ const float SCALE = 2.4;
38
+
39
+ float hash(vec2 p) {
40
+ vec3 p3 = fract(vec3(p.xyx) * 0.1031);
41
+ p3 += dot(p3, p3.yzx + 33.33);
42
+ return fract((p3.x + p3.y) * p3.z);
43
+ }
44
+
45
+ float noise(vec2 p) {
46
+ vec2 i = floor(p);
47
+ vec2 f = fract(p);
48
+ vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
49
+ return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + 1.0), u.x), u.y);
50
+ }
51
+
52
+ float fbm(vec2 p) {
53
+ float v = 0.0;
54
+ float a = 0.5;
55
+ mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);
56
+ for (int i = 0; i < 3; i++) {
57
+ v += a * noise(p);
58
+ p = rot * p * 2.03 + 17.1;
59
+ a *= 0.5;
60
+ }
61
+ return v;
62
+ }
63
+
64
+ float terrain(vec2 p, vec2 hill, float t) {
65
+ vec2 warp = vec2(noise(p * 0.55 + vec2(t * 0.08, 3.1)), noise(p * 0.55 + vec2(7.7, -t * 0.07)));
66
+ float h = fbm(p + warp * 1.3 + vec2(t * 0.035, -t * 0.025));
67
+ vec2 d = p - hill;
68
+ return h + 0.2 * exp(-dot(d, d) * 1.6);
69
+ }
70
+
71
+ void main() {
72
+ vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y);
73
+ float minRes = min(uResolution.x, uResolution.y);
74
+ float px = SCALE / minRes;
75
+ vec2 p = frag * px;
76
+ vec2 hill = uPointer * px;
77
+ float t = uTime;
78
+
79
+ float h = terrain(p, hill, t);
80
+ float hx = terrain(p + vec2(px, 0.0), hill, t);
81
+ float hy = terrain(p + vec2(0.0, px), hill, t);
82
+
83
+ float f = h * uLevels;
84
+ float slope = max(length(vec2(hx - h, hy - h)) * uLevels, 1e-5);
85
+ float fr = fract(f);
86
+ float dist = min(fr, 1.0 - fr) / slope;
87
+
88
+ float index = floor(f + 0.5);
89
+ float major = 1.0 - step(0.5, abs(mod(index, 5.0)));
90
+ float halfWidth = mix(0.5, 0.95, major) * uDpr;
91
+ float line = 1.0 - smoothstep(halfWidth - 0.6, halfWidth + 0.6, dist);
92
+
93
+ // 线间距小于数像素时淡出,避免陡坡处糊成一片
94
+ line *= smoothstep(2.5 * uDpr, 6.0 * uDpr, 1.0 / slope);
95
+
96
+ float radius = 240.0 * uDpr;
97
+ vec2 dp = frag - uPointer;
98
+ float near = exp(-dot(dp, dp) / (radius * radius));
99
+
100
+ vec2 q = (frag - 0.5 * uResolution) / (0.5 * uResolution);
101
+ float mask = mix(0.4, 1.0, 1.0 - smoothstep(0.45, 1.4, length(q)));
102
+
103
+ float alpha = line * mix(0.32, 0.7, major) * (1.0 + 0.9 * near) * mask;
104
+ vec3 lineColor = mix(uLine, uHighlight, clamp(near * 1.2, 0.0, 1.0));
105
+ vec3 color = mix(uBg, lineColor, clamp(alpha, 0.0, 1.0));
106
+
107
+ color = pow(color, vec3(1.0 / 2.2));
108
+ color += (hash(frag + fract(t)) - 0.5) / 255.0;
109
+
110
+ gl_FragColor = vec4(color, 1.0);
111
+ }
112
+ `;
113
+
114
+ export function Contours({
115
+ children,
116
+ className,
117
+ color = '#8a90a6',
118
+ highlightColor = '#f08a4b',
119
+ density = 1,
120
+ speed = 1,
121
+ interactive = true,
122
+ }: ContoursProps) {
123
+ const scheme = useColorScheme();
124
+ const canvasRef = useShaderCanvas({
125
+ fragmentShader: FRAGMENT_SHADER,
126
+ speed,
127
+ interactive,
128
+ onDraw: ({ gl, uniform, dpr }) => {
129
+ gl.uniform3fv(uniform('uLine'), hexToLinearRgb(color));
130
+ gl.uniform3fv(uniform('uHighlight'), hexToLinearRgb(highlightColor));
131
+ gl.uniform3fv(uniform('uBg'), hexToLinearRgb(scheme === 'light' ? '#f9f9f7' : '#0a0b0f'));
132
+ gl.uniform1f(uniform('uLevels'), 18 * Math.max(0.2, density));
133
+ gl.uniform1f(uniform('uDpr'), dpr);
134
+ },
135
+ });
136
+
137
+ return (
138
+ <div
139
+ className={cn('elyri-contours', className)}
140
+ data-scheme={scheme}
141
+ style={{ '--elyri-contours-color': color } as CSSProperties}
142
+ >
143
+ <canvas ref={canvasRef} className="elyri-contours__canvas" aria-hidden="true" />
144
+ {children != null && <div className="elyri-contours__content">{children}</div>}
145
+ </div>
146
+ );
147
+ }
@@ -0,0 +1,2 @@
1
+ export { Contours } from './Contours';
2
+ export type { ContoursProps } from './Contours';
@@ -0,0 +1,44 @@
1
+ .elyri-dot-field {
2
+ --elyri-dot-field-color: #8a90a6;
3
+ --elyri-dot-field-gap: 22px;
4
+ --elyri-dot-field-bg: #0a0a0e;
5
+
6
+ position: relative;
7
+ isolation: isolate;
8
+ overflow: hidden;
9
+ /* WebGL 不可用或尚未绘制时的底色:静态点阵 */
10
+ background:
11
+ radial-gradient(
12
+ circle at center,
13
+ color-mix(in srgb, var(--elyri-dot-field-color) 55%, transparent) 1px,
14
+ transparent 1.6px
15
+ )
16
+ 0 0 / var(--elyri-dot-field-gap) var(--elyri-dot-field-gap),
17
+ var(--elyri-dot-field-bg);
18
+ }
19
+
20
+ .elyri-dot-field[data-scheme='light'] {
21
+ --elyri-dot-field-bg: #fafafb;
22
+ }
23
+
24
+ .elyri-dot-field__canvas {
25
+ position: absolute;
26
+ inset: 0;
27
+ z-index: 0;
28
+ display: block;
29
+ width: 100%;
30
+ height: 100%;
31
+ opacity: 0;
32
+ pointer-events: none;
33
+ transition: opacity var(--elyri-duration-slow) var(--elyri-ease-out);
34
+ }
35
+
36
+ .elyri-dot-field__canvas[data-ready] {
37
+ opacity: 1;
38
+ }
39
+
40
+ .elyri-dot-field__content {
41
+ position: relative;
42
+ z-index: 1;
43
+ height: 100%;
44
+ }
@@ -0,0 +1,135 @@
1
+ import type { CSSProperties, ReactNode } from 'react';
2
+
3
+ import { cn, hexToLinearRgb, useColorScheme, useShaderCanvas } from '../../../core';
4
+
5
+ import './DotField.css';
6
+
7
+ export interface DotFieldProps {
8
+ children?: ReactNode;
9
+ className?: string;
10
+ /** 点的底色(hex) */
11
+ color?: string;
12
+ /** 波峰与指针附近的点所用的强调色(hex) */
13
+ highlightColor?: string;
14
+ /** 点阵间距(CSS 像素) */
15
+ gap?: number;
16
+ /** 动画速度倍率 */
17
+ speed?: number;
18
+ /** 指针附近的点是否被放大、推开 */
19
+ interactive?: boolean;
20
+ }
21
+
22
+ // 呼吸点阵:规整网格上的细点,被两层缓慢漂移的噪声调制大小与亮度,
23
+ // 像一阵阵微风掠过;指针处形成一片透镜,点被放大、向外轻推并染上强调色。
24
+ // 每个像素检查 3×3 邻格,保证被推开的点不会被格子边界裁掉。
25
+ const FRAGMENT_SHADER = `
26
+ precision highp float;
27
+
28
+ uniform vec2 uResolution;
29
+ uniform float uTime;
30
+ uniform vec2 uPointer;
31
+ uniform vec3 uDot;
32
+ uniform vec3 uHighlight;
33
+ uniform vec3 uBg;
34
+ uniform float uGap;
35
+ uniform float uDpr;
36
+
37
+ float hash(vec2 p) {
38
+ vec3 p3 = fract(vec3(p.xyx) * 0.1031);
39
+ p3 += dot(p3, p3.yzx + 33.33);
40
+ return fract((p3.x + p3.y) * p3.z);
41
+ }
42
+
43
+ float noise(vec2 p) {
44
+ vec2 i = floor(p);
45
+ vec2 f = fract(p);
46
+ vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
47
+ return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + 1.0), u.x), u.y);
48
+ }
49
+
50
+ void main() {
51
+ vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y);
52
+ float gap = uGap * uDpr;
53
+ float radius = 170.0 * uDpr;
54
+ float t = uTime;
55
+ vec2 cell = floor(frag / gap);
56
+
57
+ float best = 0.0;
58
+ float bestTint = 0.0;
59
+ for (int j = -1; j <= 1; j++) {
60
+ for (int i = -1; i <= 1; i++) {
61
+ vec2 id = cell + vec2(float(i), float(j));
62
+ vec2 center = (id + 0.5) * gap;
63
+
64
+ vec2 away = center - uPointer;
65
+ float d = length(away);
66
+ float lens = exp(-d * d / (radius * radius));
67
+ center += away / max(d, 1.0) * lens * gap * 0.32;
68
+
69
+ // 两层噪声:大尺度的「风」+ 小尺度的细碎起伏
70
+ float wave = noise(id * 0.075 + vec2(t * 0.11, -t * 0.07)) * 0.65
71
+ + noise(id * 0.21 - vec2(t * 0.05, t * 0.09)) * 0.35;
72
+ wave = smoothstep(0.3, 0.85, wave);
73
+
74
+ float r = max(gap * (0.055 + 0.075 * wave + 0.1 * lens), 0.7 * uDpr);
75
+ float cover = 1.0 - smoothstep(r - 0.8, r + 0.8, length(frag - center));
76
+ float alpha = cover * clamp(0.22 + 0.5 * wave + 0.55 * lens, 0.0, 1.0);
77
+ if (alpha > best) {
78
+ best = alpha;
79
+ bestTint = clamp(wave * wave * 0.55 + lens * 0.95, 0.0, 1.0);
80
+ }
81
+ }
82
+ }
83
+
84
+ // 四周渐隐,视线聚焦在中央
85
+ vec2 q = (frag - 0.5 * uResolution) / (0.5 * uResolution);
86
+ float mask = mix(0.18, 1.0, 1.0 - smoothstep(0.3, 1.3, length(q)));
87
+
88
+ vec3 dotColor = mix(uDot, uHighlight, bestTint);
89
+ vec3 color = mix(uBg, dotColor, best * mask);
90
+
91
+ // 指针下方极淡的光晕
92
+ float glow = exp(-dot(frag - uPointer, frag - uPointer) / (radius * radius * 2.5));
93
+ color += uHighlight * glow * 0.035;
94
+
95
+ color = pow(color, vec3(1.0 / 2.2));
96
+ color += (hash(frag + fract(t)) - 0.5) / 255.0;
97
+
98
+ gl_FragColor = vec4(color, 1.0);
99
+ }
100
+ `;
101
+
102
+ export function DotField({
103
+ children,
104
+ className,
105
+ color = '#8a90a6',
106
+ highlightColor = '#7c6cff',
107
+ gap = 22,
108
+ speed = 1,
109
+ interactive = true,
110
+ }: DotFieldProps) {
111
+ const scheme = useColorScheme();
112
+ const canvasRef = useShaderCanvas({
113
+ fragmentShader: FRAGMENT_SHADER,
114
+ speed,
115
+ interactive,
116
+ onDraw: ({ gl, uniform, dpr }) => {
117
+ gl.uniform3fv(uniform('uDot'), hexToLinearRgb(color));
118
+ gl.uniform3fv(uniform('uHighlight'), hexToLinearRgb(highlightColor));
119
+ gl.uniform3fv(uniform('uBg'), hexToLinearRgb(scheme === 'light' ? '#fafafb' : '#0a0a0e'));
120
+ gl.uniform1f(uniform('uGap'), Math.max(8, gap));
121
+ gl.uniform1f(uniform('uDpr'), dpr);
122
+ },
123
+ });
124
+
125
+ return (
126
+ <div
127
+ className={cn('elyri-dot-field', className)}
128
+ data-scheme={scheme}
129
+ style={{ '--elyri-dot-field-color': color, '--elyri-dot-field-gap': `${Math.max(8, gap)}px` } as CSSProperties}
130
+ >
131
+ <canvas ref={canvasRef} className="elyri-dot-field__canvas" aria-hidden="true" />
132
+ {children != null && <div className="elyri-dot-field__content">{children}</div>}
133
+ </div>
134
+ );
135
+ }
@@ -0,0 +1,2 @@
1
+ export { DotField } from './DotField';
2
+ export type { DotFieldProps } from './DotField';
@@ -0,0 +1,44 @@
1
+ .elyri-dragon-scales {
2
+ --elyri-dragon-scales-color: #7b3fe4;
3
+
4
+ position: relative;
5
+ isolation: isolate;
6
+ overflow: hidden;
7
+ /* WebGL 不可用或尚未绘制时的底色 */
8
+ background: radial-gradient(
9
+ 120% 90% at 50% 30%,
10
+ color-mix(in srgb, var(--elyri-dragon-scales-color) 30%, #000),
11
+ #030206 75%
12
+ );
13
+ }
14
+
15
+ /* 浅色主题:兜底底色同步翻转为浅色,避免就绪前闪深色 */
16
+ .elyri-dragon-scales[data-scheme='light'] {
17
+ background: radial-gradient(
18
+ 120% 90% at 50% 30%,
19
+ #ffffff,
20
+ color-mix(in srgb, var(--elyri-dragon-scales-color) 12%, #ffffff) 75%
21
+ );
22
+ }
23
+
24
+ .elyri-dragon-scales__canvas {
25
+ position: absolute;
26
+ inset: 0;
27
+ z-index: 0;
28
+ display: block;
29
+ width: 100%;
30
+ height: 100%;
31
+ opacity: 0;
32
+ pointer-events: none;
33
+ transition: opacity var(--elyri-duration-slow) var(--elyri-ease-out);
34
+ }
35
+
36
+ .elyri-dragon-scales__canvas[data-ready] {
37
+ opacity: 1;
38
+ }
39
+
40
+ .elyri-dragon-scales__content {
41
+ position: relative;
42
+ z-index: 1;
43
+ height: 100%;
44
+ }
@@ -0,0 +1,250 @@
1
+ import type { CSSProperties, ReactNode } from 'react';
2
+
3
+ import type { ColorScheme } from '../../../core';
4
+ import { cn, hexToLinearRgb, useColorScheme, useShaderCanvas } from '../../../core';
5
+
6
+ import './DragonScales.css';
7
+
8
+ export interface DragonScalesProps {
9
+ children?: ReactNode;
10
+ className?: string;
11
+ /** 鳞片本体色(hex) */
12
+ color?: string;
13
+ /** 反光条与鳞缘高光色(hex) */
14
+ highlightColor?: string;
15
+ /** 单片鳞宽度(px) */
16
+ scaleSize?: number;
17
+ /** 动画速度倍率 */
18
+ speed?: number;
19
+ /** 鳞片开合幅度 0-1,0 为关闭 */
20
+ flare?: number;
21
+ /** 光源是否跟随指针 */
22
+ interactive?: boolean;
23
+ /** 强制明暗方案,默认跟随页面主题 */
24
+ scheme?: ColorScheme;
25
+ }
26
+
27
+ // 鳞片为尖头鱼鳞形(vesica),侧向排列:鳞尖朝右,左侧鳞片压住右侧。
28
+ // 表面是黑曜石般的高光釉面:窄倒角 + 平缓鼓面,反射一组线形灯带,
29
+ // 灯带在曲面上被拉成贴着轮廓的弧形亮线,鳞缘再勾一道细亮边。
30
+ const FRAGMENT_SHADER = `
31
+ precision highp float;
32
+
33
+ uniform vec2 uResolution;
34
+ uniform float uTime;
35
+ uniform vec2 uPointer;
36
+ uniform float uScale;
37
+ uniform vec3 uBase;
38
+ uniform vec3 uHighlight;
39
+ uniform float uFlare;
40
+ uniform float uScheme;
41
+
42
+ const float R = 0.92;
43
+ const float D = 0.30;
44
+ const float B = 0.8697;
45
+ const float ROW = 0.5;
46
+ const float BEVEL = 0.075;
47
+ const float TAU = 6.28318;
48
+
49
+ float hash(vec2 p) {
50
+ p = fract(p * vec2(123.34, 456.21));
51
+ p += dot(p, p + 45.32);
52
+ return fract(p.x * p.y);
53
+ }
54
+
55
+ float sdScale(vec2 p) {
56
+ p = abs(p);
57
+ return ((p.y - B) * D > p.x * B) ? length(p - vec2(0.0, B)) : length(p - vec2(-D, 0.0)) - R;
58
+ }
59
+
60
+ float scaleHeight(vec2 q) {
61
+ float inner = clamp(-sdScale(q) / BEVEL, 0.0, 1.0);
62
+ float bevel = sqrt(1.0 - (1.0 - inner) * (1.0 - inner));
63
+ vec2 d = vec2(q.x / 0.62, (q.y - 0.35) / 0.75);
64
+ float dome = 0.14 * (1.0 - dot(d, d));
65
+ float keel = 0.035 * exp(-q.x * q.x / 0.03) * smoothstep(-0.1, B, q.y);
66
+ return bevel * (0.07 + dome + keel);
67
+ }
68
+
69
+ // 开合:一道波沿身体方向推进,经过的鳞片依次张开再合拢
70
+ float openAmount(vec2 c) {
71
+ return uFlare * smoothstep(0.15, 1.0, sin(c.y * 0.45 - uTime * 1.3 + c.x * 0.3));
72
+ }
73
+
74
+ // 张开时鳞片绕根部翘起,透视上表现为鳞尖伸长、略微变宽
75
+ vec2 warp(vec2 q, float o) {
76
+ return vec2(q.x / (1.0 + 0.05 * o), (q.y + 0.2) / (1.0 + 0.2 * o) - 0.2);
77
+ }
78
+
79
+ vec3 aces(vec3 x) {
80
+ return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
81
+ }
82
+
83
+ void main() {
84
+ vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y);
85
+ // 形状空间的 y(鳞尖方向)对应屏幕 x,实现侧向排列
86
+ vec2 p = frag.yx / uScale;
87
+ float px = 1.0 / uScale;
88
+ float t = uTime;
89
+
90
+ // 从左到右查找第一片覆盖当前像素的鳞,并记录到压在上面的鳞片边缘的距离。
91
+ // 阴影与缝隙散光按每片上方鳞分别计算后取最大值,避免在两片张开程度不同的鳞之间出现接缝
92
+ float j0 = floor(p.y / ROW);
93
+ vec2 center = vec2(0.0);
94
+ vec2 cell = vec2(0.0);
95
+ float sd = -1.0;
96
+ float cover = 1.0;
97
+ float shade = 0.0;
98
+ float gap = 0.0;
99
+ float open = 0.0;
100
+ float above = 1.0;
101
+ float aboveShade = 0.0;
102
+ float aboveGap = 0.0;
103
+ float found = 0.0;
104
+ for (int dj = -2; dj <= 2; dj++) {
105
+ float j = j0 + float(dj);
106
+ float offset = mod(j, 2.0) * 0.5;
107
+ float i0 = floor(p.x - offset + 0.5);
108
+ float rowMin = 1.0;
109
+ float rowShade = 0.0;
110
+ float rowGap = 0.0;
111
+ for (int di = -1; di <= 1; di++) {
112
+ vec2 id = vec2(i0 + float(di), j);
113
+ vec2 c = vec2(id.x + offset, j * ROW);
114
+ float o = openAmount(c);
115
+ float d = sdScale(warp(p - c, o));
116
+ if (found < 0.5 && d < 0.0) {
117
+ found = 1.0;
118
+ center = c;
119
+ cell = id;
120
+ sd = d;
121
+ open = o;
122
+ cover = above;
123
+ shade = aboveShade;
124
+ gap = aboveGap;
125
+ }
126
+ rowMin = min(rowMin, d);
127
+ // 上方鳞片张开时投下更宽的阴影,下方透出散射光
128
+ rowShade = max(rowShade, 1.0 - smoothstep(0.0, 0.16 + 0.3 * o, d));
129
+ rowGap = max(rowGap, (1.0 - smoothstep(0.0, 0.1 + 0.25 * o, d)) * o);
130
+ }
131
+ if (found < 0.5) {
132
+ above = min(above, rowMin);
133
+ aboveShade = max(aboveShade, rowShade);
134
+ aboveGap = max(aboveGap, rowGap);
135
+ }
136
+ }
137
+
138
+ vec2 q = warp(p - center, open);
139
+ float seed = hash(cell);
140
+ float seed2 = hash(cell + 17.31);
141
+
142
+ float e = 0.006;
143
+ vec2 grad = vec2(
144
+ scaleHeight(q + vec2(e, 0.0)) - scaleHeight(q - vec2(e, 0.0)),
145
+ scaleHeight(q + vec2(0.0, e)) - scaleHeight(q - vec2(0.0, e))
146
+ ) / (2.0 * e);
147
+ vec3 ns = normalize(vec3(-grad, 1.0));
148
+
149
+ // 每片鳞各自略微倾斜,再叠一道沿身体方向滚动的起伏,让反光条在鳞间流动
150
+ float wave = sin(center.y * 0.35 - t * 0.9 + sin(center.x * 0.2 + t * 0.25) * 1.2);
151
+ ns = normalize(ns + vec3((seed - 0.5) * 0.22, (seed2 - 0.5) * 0.22 + wave * 0.1 - open * 0.45, 0.0));
152
+ vec3 n = vec3(ns.y, ns.x, ns.z);
153
+
154
+ float minRes = min(uResolution.x, uResolution.y);
155
+ vec3 V = vec3(0.0, 0.0, 1.0);
156
+ vec3 toLight = vec3(uPointer - frag, minRes * 0.6);
157
+ float dist = length(toLight);
158
+ vec3 L = toLight / dist;
159
+ float atten = 1.0 / (1.0 + 1.5 * pow(dist / minRes, 2.0));
160
+ float NdotH = max(dot(n, normalize(L + V)), 0.0);
161
+ float NdotV = max(n.z, 0.0);
162
+ float soft = pow(NdotH, 24.0) * atten;
163
+ float ao = mix(0.04, 1.0, 1.0 - shade);
164
+ float fres = 0.35 + 0.65 * pow(1.0 - NdotV, 2.0);
165
+
166
+ // 线形灯带:主灯带穿过光源位置并缓慢转动,副灯带只落在朝向一侧的倒角上
167
+ vec3 Rf = reflect(-V, n);
168
+ vec2 toL = (uPointer - frag) / minRes;
169
+ float a = 0.7 + 0.45 * sin(t * 0.17);
170
+ vec2 dir = vec2(cos(a), sin(a));
171
+ float s1 = dot(Rf.xy, dir) - dot(toL, dir) * 1.3 + 0.12 * sin(t * 0.5 + seed * TAU);
172
+ float line1 = exp(-s1 * s1 / 0.0022);
173
+ float band1 = exp(-s1 * s1 / 0.09);
174
+ float s2 = dot(Rf.xy, vec2(-dir.y, dir.x)) - 0.85 - 0.15 * sin(t * 0.33);
175
+ float line2 = exp(-s2 * s2 / 0.01);
176
+
177
+ // 本体近黑,只在朝向光源的区域泛出一层柔和的底色,亮度主要来自反光条
178
+ vec3 bloom = mix(uBase, uHighlight, 0.35);
179
+ vec3 color = uBase * (0.006 + 0.5 * soft * mix(0.6, 1.1, seed)) * ao;
180
+ color += bloom * band1 * 0.3 * atten * ao;
181
+ color += uHighlight * line1 * 1.4 * fres * (0.3 + atten) * ao;
182
+ color += uHighlight * line2 * 0.18 * fres * ao;
183
+
184
+ // 双线鳞缘:上层鳞的边与被压住的下层鳞的边各一道细亮线,中间留一条暗缝
185
+ float rimLight = (0.07 + 0.6 * soft + band1 * 0.9) * (1.0 + 1.2 * open);
186
+ float r1 = (sd + 1.6 * px) / (1.4 * px);
187
+ float r2 = (cover - 1.8 * px) / (1.4 * px);
188
+ color += uHighlight * exp(-r1 * r1) * rimLight * 0.9;
189
+ color += uHighlight * exp(-r2 * r2) * rimLight * 0.35;
190
+
191
+ color += mix(uBase, uHighlight, 0.2) * gap * 0.35 * (0.3 + atten + band1);
192
+
193
+ color += uHighlight * 0.03 * exp(-pow(length(frag - uPointer) / (minRes * 0.4), 2.0));
194
+
195
+ vec2 vc = frag / uResolution - 0.5;
196
+ float vig = dot(vc, vc);
197
+
198
+ if (uScheme > 0.5) {
199
+ // 浅色:先色调映射,再做亮暗镜像,保留色相 —— 黑曜石鳞面翻转成浅色鳞面,反光条转为深色鳞缘
200
+ vec3 c = pow(aces(color * 1.3), vec3(1.0 / 2.2));
201
+ float lum = dot(c, vec3(0.2126, 0.7152, 0.0722));
202
+ color = clamp(c + (1.0 - 2.0 * lum), 0.0, 1.0) * (1.0 - vig * 0.35);
203
+ } else {
204
+ color *= 1.0 - vig * 1.5;
205
+ color = pow(aces(color * 1.3), vec3(1.0 / 2.2));
206
+ }
207
+
208
+ color += (hash(frag + fract(t)) - 0.5) / 255.0;
209
+
210
+ gl_FragColor = vec4(color, 1.0);
211
+ }
212
+ `;
213
+
214
+ export function DragonScales({
215
+ children,
216
+ className,
217
+ color = '#7b3fe4',
218
+ highlightColor = '#e7a6ff',
219
+ scaleSize = 110,
220
+ speed = 1,
221
+ flare = 0,
222
+ interactive = true,
223
+ scheme: forcedScheme,
224
+ }: DragonScalesProps) {
225
+ const detectedScheme = useColorScheme();
226
+ const scheme = forcedScheme ?? detectedScheme;
227
+ const canvasRef = useShaderCanvas({
228
+ fragmentShader: FRAGMENT_SHADER,
229
+ speed,
230
+ interactive,
231
+ onDraw: ({ gl, uniform, dpr }) => {
232
+ gl.uniform1f(uniform('uScale'), Math.max(8, scaleSize) * dpr);
233
+ gl.uniform3fv(uniform('uBase'), hexToLinearRgb(color));
234
+ gl.uniform3fv(uniform('uHighlight'), hexToLinearRgb(highlightColor));
235
+ gl.uniform1f(uniform('uFlare'), Math.min(Math.max(flare, 0), 1));
236
+ gl.uniform1f(uniform('uScheme'), scheme === 'light' ? 1 : 0);
237
+ },
238
+ });
239
+
240
+ return (
241
+ <div
242
+ className={cn('elyri-dragon-scales', className)}
243
+ data-scheme={scheme}
244
+ style={{ '--elyri-dragon-scales-color': color } as CSSProperties}
245
+ >
246
+ <canvas ref={canvasRef} className="elyri-dragon-scales__canvas" aria-hidden="true" />
247
+ {children != null && <div className="elyri-dragon-scales__content">{children}</div>}
248
+ </div>
249
+ );
250
+ }
@@ -0,0 +1,2 @@
1
+ export { DragonScales } from './DragonScales';
2
+ export type { DragonScalesProps } from './DragonScales';
@@ -0,0 +1,38 @@
1
+ .elyri-filaments {
2
+ --elyri-filaments-color: #5b8cff;
3
+ --elyri-filaments-bg: #06060a;
4
+
5
+ position: relative;
6
+ isolation: isolate;
7
+ overflow: hidden;
8
+ /* WebGL 不可用或尚未绘制时的底色:中央一道柔光 */
9
+ background:
10
+ radial-gradient(60% 14% at 50% 50%, color-mix(in srgb, var(--elyri-filaments-color) 30%, transparent), transparent),
11
+ var(--elyri-filaments-bg);
12
+ }
13
+
14
+ .elyri-filaments[data-scheme='light'] {
15
+ --elyri-filaments-bg: #f7f7fa;
16
+ }
17
+
18
+ .elyri-filaments__canvas {
19
+ position: absolute;
20
+ inset: 0;
21
+ z-index: 0;
22
+ display: block;
23
+ width: 100%;
24
+ height: 100%;
25
+ opacity: 0;
26
+ pointer-events: none;
27
+ transition: opacity var(--elyri-duration-slow) var(--elyri-ease-out);
28
+ }
29
+
30
+ .elyri-filaments__canvas[data-ready] {
31
+ opacity: 1;
32
+ }
33
+
34
+ .elyri-filaments__content {
35
+ position: relative;
36
+ z-index: 1;
37
+ height: 100%;
38
+ }