@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,138 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn, hexToLinearRgb, useColorScheme, useShaderCanvas } from '../../../core';
|
|
4
|
+
|
|
5
|
+
import './Filaments.css';
|
|
6
|
+
|
|
7
|
+
export interface FilamentsProps {
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
/** 光丝主色(hex) */
|
|
11
|
+
color?: string;
|
|
12
|
+
/** 沿光丝渐变的第二种颜色(hex) */
|
|
13
|
+
highlightColor?: string;
|
|
14
|
+
/** 光丝振幅倍率 */
|
|
15
|
+
amplitude?: number;
|
|
16
|
+
/** 动画速度倍率 */
|
|
17
|
+
speed?: number;
|
|
18
|
+
/** 指针处的光丝是否被拨动、弯向指针 */
|
|
19
|
+
interactive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 光丝:七根发丝般的光线沿水平方向交织成一束缓慢起伏的波形,像被轻拨的琴弦;
|
|
23
|
+
// 每根的振幅、频率与相位各不相同,光脉冲沿线流过,颜色沿横向在两色之间渐变。
|
|
24
|
+
// 用解析曲线求有符号距离并按斜率修正,线宽处处一致;RGB 轻微错位带出色散边。
|
|
25
|
+
const FRAGMENT_SHADER = `
|
|
26
|
+
precision highp float;
|
|
27
|
+
|
|
28
|
+
uniform vec2 uResolution;
|
|
29
|
+
uniform float uTime;
|
|
30
|
+
uniform vec2 uPointer;
|
|
31
|
+
uniform vec3 uColor;
|
|
32
|
+
uniform vec3 uHighlight;
|
|
33
|
+
uniform vec3 uBg;
|
|
34
|
+
uniform float uAmplitude;
|
|
35
|
+
uniform float uDpr;
|
|
36
|
+
uniform float uScheme;
|
|
37
|
+
|
|
38
|
+
const int COUNT = 7;
|
|
39
|
+
|
|
40
|
+
float hash(vec2 p) {
|
|
41
|
+
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
42
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
43
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
vec3 shade(vec3 bg, vec3 light) {
|
|
47
|
+
if (uScheme > 0.5) {
|
|
48
|
+
float amount = max(max(light.r, light.g), light.b);
|
|
49
|
+
vec3 tint = light / max(amount, 1e-4);
|
|
50
|
+
return bg * exp(-amount * 1.4 * (1.0 - tint * 0.82));
|
|
51
|
+
}
|
|
52
|
+
return bg + 1.0 - exp(-light);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
float glow(float d, float w) {
|
|
56
|
+
return exp(-d * d / (w * w)) + 0.22 * exp(-abs(d) / (w * 5.0)) + 0.035 * exp(-abs(d) / 0.05);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
float threadY(float x, float fi, float t, vec2 m) {
|
|
60
|
+
float ph = fi * 1.7;
|
|
61
|
+
float env = exp(-x * x * 1.1);
|
|
62
|
+
float pluck = exp(-pow((x - m.x) / 0.35, 2.0));
|
|
63
|
+
float amp = (0.05 + 0.09 * pow(sin(fi * 1.3 + 0.5), 2.0)) * uAmplitude + pluck * 0.05;
|
|
64
|
+
return env * (amp * sin(x * (2.1 + fi * 0.09) + t * (1.0 + 0.12 * fi) + ph) + 0.028 * uAmplitude * sin(x * 4.3 - t * 0.7 + ph * 2.0))
|
|
65
|
+
+ m.y * 0.22 * pluck;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
void main() {
|
|
69
|
+
vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y);
|
|
70
|
+
float minRes = min(uResolution.x, uResolution.y);
|
|
71
|
+
vec2 uv = (frag - 0.5 * uResolution) / minRes;
|
|
72
|
+
vec2 m = (uPointer - 0.5 * uResolution) / minRes;
|
|
73
|
+
float t = uTime * 0.35;
|
|
74
|
+
float hx = 0.5 * uResolution.x / minRes;
|
|
75
|
+
|
|
76
|
+
float w = 0.9 * uDpr / minRes;
|
|
77
|
+
float shift = 1.6 * uDpr / minRes;
|
|
78
|
+
float e = 0.002;
|
|
79
|
+
float fade = smoothstep(hx * 1.02, hx * 0.3, abs(uv.x));
|
|
80
|
+
|
|
81
|
+
vec3 light = vec3(0.0);
|
|
82
|
+
for (int i = 0; i < COUNT; i++) {
|
|
83
|
+
float fi = float(i);
|
|
84
|
+
float y = threadY(uv.x, fi, t, m);
|
|
85
|
+
float dy = (threadY(uv.x + e, fi, t, m) - y) / e;
|
|
86
|
+
float s = (uv.y - y) / sqrt(1.0 + dy * dy);
|
|
87
|
+
|
|
88
|
+
float ph = fi * 1.7;
|
|
89
|
+
float pulse = pow(0.5 + 0.5 * sin(uv.x * 1.6 - t * 1.4 + ph * 2.3), 6.0);
|
|
90
|
+
float energy = (0.22 + 1.1 * pulse) * fade * (0.6 + 0.4 * sin(fi * 2.1 + 1.0) * sin(fi * 2.1 + 1.0));
|
|
91
|
+
vec3 tint = mix(uColor, uHighlight, 0.5 + 0.5 * sin(uv.x * 1.2 + ph + t * 0.3));
|
|
92
|
+
|
|
93
|
+
vec3 g = vec3(glow(s - shift, w), glow(s, w), glow(s + shift, w));
|
|
94
|
+
light += tint * g * energy;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
vec3 color = pow(clamp(shade(uBg, light), 0.0, 1.0), vec3(1.0 / 2.2));
|
|
98
|
+
color += (hash(frag + fract(t)) - 0.5) / 255.0;
|
|
99
|
+
|
|
100
|
+
gl_FragColor = vec4(color, 1.0);
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
export function Filaments({
|
|
105
|
+
children,
|
|
106
|
+
className,
|
|
107
|
+
color = '#5b8cff',
|
|
108
|
+
highlightColor = '#d46bff',
|
|
109
|
+
amplitude = 1,
|
|
110
|
+
speed = 1,
|
|
111
|
+
interactive = true,
|
|
112
|
+
}: FilamentsProps) {
|
|
113
|
+
const scheme = useColorScheme();
|
|
114
|
+
const canvasRef = useShaderCanvas({
|
|
115
|
+
fragmentShader: FRAGMENT_SHADER,
|
|
116
|
+
speed,
|
|
117
|
+
interactive,
|
|
118
|
+
onDraw: ({ gl, uniform, dpr }) => {
|
|
119
|
+
gl.uniform3fv(uniform('uColor'), hexToLinearRgb(color));
|
|
120
|
+
gl.uniform3fv(uniform('uHighlight'), hexToLinearRgb(highlightColor));
|
|
121
|
+
gl.uniform3fv(uniform('uBg'), hexToLinearRgb(scheme === 'light' ? '#f7f7fa' : '#06060a'));
|
|
122
|
+
gl.uniform1f(uniform('uAmplitude'), Math.max(0, amplitude));
|
|
123
|
+
gl.uniform1f(uniform('uDpr'), dpr);
|
|
124
|
+
gl.uniform1f(uniform('uScheme'), scheme === 'light' ? 1 : 0);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
className={cn('elyri-filaments', className)}
|
|
131
|
+
data-scheme={scheme}
|
|
132
|
+
style={{ '--elyri-filaments-color': color } as CSSProperties}
|
|
133
|
+
>
|
|
134
|
+
<canvas ref={canvasRef} className="elyri-filaments__canvas" aria-hidden="true" />
|
|
135
|
+
{children != null && <div className="elyri-filaments__content">{children}</div>}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.elyri-flip-tiles {
|
|
2
|
+
--elyri-flip-tiles-color: #4f6bff;
|
|
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-flip-tiles-color) 14%, #0c0c12),
|
|
11
|
+
#030305 75%
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* 浅色主题:兜底底色同步翻转为浅色,避免就绪前闪深色 */
|
|
16
|
+
.elyri-flip-tiles[data-scheme='light'] {
|
|
17
|
+
background: radial-gradient(
|
|
18
|
+
120% 90% at 50% 30%,
|
|
19
|
+
#f4f4f7,
|
|
20
|
+
color-mix(in srgb, var(--elyri-flip-tiles-color) 10%, #dcdce3) 75%
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.elyri-flip-tiles__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-flip-tiles__canvas[data-ready] {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.elyri-flip-tiles__content {
|
|
41
|
+
position: relative;
|
|
42
|
+
z-index: 1;
|
|
43
|
+
height: 100%;
|
|
44
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn, hexToLinearRgb, useColorScheme, useShaderCanvas } from '../../../core';
|
|
4
|
+
|
|
5
|
+
import './FlipTiles.css';
|
|
6
|
+
|
|
7
|
+
export type FlipTilesPattern = 'wave' | 'ripple' | 'drift';
|
|
8
|
+
|
|
9
|
+
export interface FlipTilesProps {
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
/** 立方体侧面的强调色(hex),翻滚时翻上来的就是这一面 */
|
|
13
|
+
color?: string;
|
|
14
|
+
/** 金属倒角与反光条的高光色(hex) */
|
|
15
|
+
highlightColor?: string;
|
|
16
|
+
/** 立方体间距(px),即单个立方体加缝隙的宽度 */
|
|
17
|
+
tileSize?: number;
|
|
18
|
+
/** 动画速度倍率 */
|
|
19
|
+
speed?: number;
|
|
20
|
+
/** 翻滚编排:成列扫过、中心涟漪或有机漂移 */
|
|
21
|
+
pattern?: FlipTilesPattern;
|
|
22
|
+
/** 指针处立方体升起(微凸)是否跟随指针 */
|
|
23
|
+
interactive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PATTERNS: FlipTilesPattern[] = ['wave', 'ripple', 'drift'];
|
|
27
|
+
|
|
28
|
+
// 立方体阵列:略带俯角的透视相机下,一片实心立方体整齐排布在深槽地面上。
|
|
29
|
+
// 顶面与底面是黑曜石镜面(浅色主题为瓷白),四个侧面是强调色金属,每条棱都有一道高光倒角。
|
|
30
|
+
// 编排波经过时立方体绕水平轴滚动 90° 把强调色侧面翻到顶上,停留片刻再滚一次回到黑曜石面;
|
|
31
|
+
// 滚动走带轻微回弹的弹簧曲线,滚动途中立方体略微收缩、跃起以免碰到邻居,露出的槽底透出辉光。
|
|
32
|
+
// 每个像素对周围 3×3 个立方体做射线求交,并向主光源投射阴影射线;远处渐隐进雾里。
|
|
33
|
+
const FRAGMENT_SHADER = `
|
|
34
|
+
precision highp float;
|
|
35
|
+
|
|
36
|
+
uniform vec2 uResolution;
|
|
37
|
+
uniform float uTime;
|
|
38
|
+
uniform vec2 uPointer;
|
|
39
|
+
uniform float uScale;
|
|
40
|
+
uniform vec3 uColor;
|
|
41
|
+
uniform vec3 uHighlight;
|
|
42
|
+
uniform vec3 uFront;
|
|
43
|
+
uniform vec3 uFloor;
|
|
44
|
+
uniform float uPattern;
|
|
45
|
+
uniform float uScheme;
|
|
46
|
+
uniform float uSamples;
|
|
47
|
+
|
|
48
|
+
const float PI = 3.14159265;
|
|
49
|
+
const float HALF = 0.45;
|
|
50
|
+
const float BEVEL = 0.055;
|
|
51
|
+
const float FLOOR_Z = -0.45;
|
|
52
|
+
const float TILT = 0.42;
|
|
53
|
+
const vec3 KEY = vec3(-0.5, -0.42, 0.757);
|
|
54
|
+
|
|
55
|
+
float hash(vec2 p) {
|
|
56
|
+
p = fract(p * vec2(123.34, 456.21));
|
|
57
|
+
p += dot(p, p + 45.32);
|
|
58
|
+
return fract(p.x * p.y);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
float noise(vec2 p) {
|
|
62
|
+
vec2 i = floor(p);
|
|
63
|
+
vec2 f = fract(p);
|
|
64
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
65
|
+
return mix(
|
|
66
|
+
mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
|
|
67
|
+
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x),
|
|
68
|
+
f.y
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 绕单位轴 k 旋转 a 弧度(Rodrigues)
|
|
73
|
+
mat3 rotAxis(vec3 k, float a) {
|
|
74
|
+
float s = sin(a);
|
|
75
|
+
float c = cos(a);
|
|
76
|
+
float oc = 1.0 - c;
|
|
77
|
+
return mat3(
|
|
78
|
+
oc * k.x * k.x + c, oc * k.x * k.y + k.z * s, oc * k.z * k.x - k.y * s,
|
|
79
|
+
oc * k.x * k.y - k.z * s, oc * k.y * k.y + c, oc * k.y * k.z + k.x * s,
|
|
80
|
+
oc * k.z * k.x + k.y * s, oc * k.y * k.z - k.x * s, oc * k.z * k.z + c
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
mat3 transposeM(mat3 m) {
|
|
85
|
+
return mat3(
|
|
86
|
+
m[0][0], m[1][0], m[2][0],
|
|
87
|
+
m[0][1], m[1][1], m[2][1],
|
|
88
|
+
m[0][2], m[1][2], m[2][2]
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 0→1 的欠阻尼弹簧:起步零速度,约 7% 的回弹后稳定
|
|
93
|
+
float spring(float x) {
|
|
94
|
+
x = clamp(x, 0.0, 1.0);
|
|
95
|
+
float k = exp(-6.0 * x) * (cos(7.0 * x) + 6.0 / 7.0 * sin(7.0 * x));
|
|
96
|
+
return 1.0 - k * (1.0 - smoothstep(0.8, 1.0, x));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 单个立方体的姿态:滚动轴与角度、滚动时的收缩与跃起、指针处的升起
|
|
100
|
+
void cubeAt(vec2 c, vec2 pw, out mat3 inv, out vec4 geo, out float open) {
|
|
101
|
+
float h = hash(c);
|
|
102
|
+
float h2 = hash(c + 17.31);
|
|
103
|
+
vec3 axis;
|
|
104
|
+
float f;
|
|
105
|
+
if (uPattern < 0.5) {
|
|
106
|
+
float a = 0.35 + 0.3 * sin(uTime * 0.045);
|
|
107
|
+
axis = vec3(0.0, 1.0, 0.0);
|
|
108
|
+
f = uTime * 0.15 - dot(c, vec2(cos(a), sin(a))) * 0.1 + h * 0.035;
|
|
109
|
+
} else if (uPattern < 1.5) {
|
|
110
|
+
axis = abs(c.x) >= abs(c.y) ? vec3(0.0, c.x < 0.0 ? -1.0 : 1.0, 0.0) : vec3(c.y < 0.0 ? 1.0 : -1.0, 0.0, 0.0);
|
|
111
|
+
f = uTime * 0.14 - length(c) * 0.12 + h * 0.03;
|
|
112
|
+
} else {
|
|
113
|
+
axis = h2 < 0.5 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
|
|
114
|
+
f = uTime * 0.07 + noise(c * 0.16 + vec2(uTime * 0.015, -uTime * 0.01)) * 2.2 + h * 0.05;
|
|
115
|
+
}
|
|
116
|
+
// 每个周期滚两次:先把强调色侧面翻上来,停留片刻,再滚回黑曜石面
|
|
117
|
+
float fl = floor(f);
|
|
118
|
+
float fr = f - fl;
|
|
119
|
+
float roll = spring(fr / 0.2) + spring((fr - 0.26) / 0.2);
|
|
120
|
+
float ang = PI * 0.5 * (2.0 * mod(fl, 2.0) + roll);
|
|
121
|
+
open = abs(sin(2.0 * ang));
|
|
122
|
+
|
|
123
|
+
// 转到 45° 时对角线最长,按投影宽度收缩,保证与邻居始终不相交
|
|
124
|
+
float reach = HALF * (abs(cos(ang)) + abs(sin(ang)));
|
|
125
|
+
float s = min(1.0, 0.49 / reach);
|
|
126
|
+
vec2 toP = c - pw;
|
|
127
|
+
float lift = 0.3 * exp(-dot(toP, toP) / 5.0);
|
|
128
|
+
|
|
129
|
+
inv = rotAxis(axis, -ang);
|
|
130
|
+
geo = vec4(c, lift + (1.0 - s) * 0.7, HALF * s);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 射线与以原点为中心的立方体求交,返回最近交点距离(未命中为 -1)与局部法线
|
|
134
|
+
float boxHit(vec3 ro, vec3 rd, float rad, out vec3 n) {
|
|
135
|
+
rd = mix(rd, vec3(1e-6), step(abs(rd), vec3(1e-6)));
|
|
136
|
+
vec3 m = 1.0 / rd;
|
|
137
|
+
vec3 k = abs(m) * rad;
|
|
138
|
+
vec3 t1 = -m * ro - k;
|
|
139
|
+
vec3 t2 = -m * ro + k;
|
|
140
|
+
float tN = max(max(t1.x, t1.y), t1.z);
|
|
141
|
+
float tF = min(min(t2.x, t2.y), t2.z);
|
|
142
|
+
n = vec3(0.0);
|
|
143
|
+
if (tN > tF || tF < 0.0) return -1.0;
|
|
144
|
+
n = -sign(rd) * step(t1.yzx, t1.xyz) * step(t1.zxy, t1.xyz);
|
|
145
|
+
return tN;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 环境光:只保留一层极弱的均匀补光,不再有扫过画面的灯带反光
|
|
149
|
+
vec3 envLight(vec3 R) {
|
|
150
|
+
return vec3(0.008 + 0.02 * smoothstep(-0.1, 0.4, R.z));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
vec3 aces(vec3 x) {
|
|
154
|
+
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
vec2 sampleOffset(int s) {
|
|
158
|
+
if (uSamples < 1.5) return vec2(0.0);
|
|
159
|
+
if (uSamples < 2.5) return s == 0 ? vec2(-0.25) : vec2(0.25);
|
|
160
|
+
if (s == 0) return vec2(-0.125, -0.375);
|
|
161
|
+
if (s == 1) return vec2(0.375, -0.125);
|
|
162
|
+
if (s == 2) return vec2(0.125, 0.375);
|
|
163
|
+
return vec2(-0.375, 0.125);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
void main() {
|
|
167
|
+
vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y);
|
|
168
|
+
vec2 center = 0.5 * uResolution;
|
|
169
|
+
float halfW = 0.5 * max(uResolution.x, uResolution.y) / uScale;
|
|
170
|
+
|
|
171
|
+
// 略带俯角的透视相机:屏幕向下对应世界 +y,相机从 +y 一侧斜看,能看到立方体朝向观者的侧面
|
|
172
|
+
float D = halfW * 2.0;
|
|
173
|
+
vec3 ro = vec3(0.0, D * sin(TILT), D * cos(TILT));
|
|
174
|
+
vec3 fw = -normalize(ro);
|
|
175
|
+
vec3 dn = vec3(0.0, cos(TILT), -sin(TILT));
|
|
176
|
+
|
|
177
|
+
vec2 puv = (uPointer - center) / uScale;
|
|
178
|
+
vec3 prd = normalize(fw * D + vec3(puv.x, 0.0, 0.0) + dn * puv.y);
|
|
179
|
+
vec2 pw = (ro + prd * ((HALF - ro.z) / prd.z)).xy;
|
|
180
|
+
|
|
181
|
+
vec2 cuv = (frag - center) / uScale;
|
|
182
|
+
vec3 crd = normalize(fw * D + vec3(cuv.x, 0.0, 0.0) + dn * cuv.y);
|
|
183
|
+
vec2 base = floor((ro + crd * (-ro.z / crd.z)).xy + 0.5);
|
|
184
|
+
|
|
185
|
+
vec3 L = normalize(KEY);
|
|
186
|
+
// 主光源的镜面高光颜色
|
|
187
|
+
vec3 specCol = mix(vec3(1.0), uHighlight, 0.35);
|
|
188
|
+
float amb = mix(0.03, 0.45, uScheme);
|
|
189
|
+
|
|
190
|
+
// 周围 3×3 个立方体的姿态只算一次,供所有采样与阴影复用
|
|
191
|
+
mat3 invs[9];
|
|
192
|
+
vec4 geos[9];
|
|
193
|
+
float opens[9];
|
|
194
|
+
float seeds[9];
|
|
195
|
+
vec3 localL[9];
|
|
196
|
+
for (int k = 0; k < 9; k++) {
|
|
197
|
+
int row = k / 3;
|
|
198
|
+
vec2 c = base + vec2(float(k - row * 3) - 1.0, float(row) - 1.0);
|
|
199
|
+
mat3 inv;
|
|
200
|
+
vec4 geo;
|
|
201
|
+
float open;
|
|
202
|
+
cubeAt(c, pw, inv, geo, open);
|
|
203
|
+
invs[k] = inv;
|
|
204
|
+
geos[k] = geo;
|
|
205
|
+
opens[k] = open;
|
|
206
|
+
seeds[k] = hash(c + 5.17);
|
|
207
|
+
// 光源方向是常量,提前转到各立方体局部空间,省去每次采样重复的矩阵乘法
|
|
208
|
+
localL[k] = inv * L;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
vec3 acc = vec3(0.0);
|
|
212
|
+
for (int s = 0; s < 4; s++) {
|
|
213
|
+
if (float(s) >= uSamples) break;
|
|
214
|
+
vec2 uv = (frag + sampleOffset(s) - center) / uScale;
|
|
215
|
+
vec3 rd = normalize(fw * D + vec3(uv.x, 0.0, 0.0) + dn * uv.y);
|
|
216
|
+
|
|
217
|
+
float tMin = 1e9;
|
|
218
|
+
int hitK = -1;
|
|
219
|
+
vec3 hitN = vec3(0.0);
|
|
220
|
+
vec3 hitP = vec3(0.0);
|
|
221
|
+
mat3 hitInv = mat3(1.0);
|
|
222
|
+
float hitSize = HALF;
|
|
223
|
+
float hitSeed = 0.0;
|
|
224
|
+
for (int k = 0; k < 9; k++) {
|
|
225
|
+
vec3 lro = invs[k] * (ro - geos[k].xyz);
|
|
226
|
+
vec3 lrd = invs[k] * rd;
|
|
227
|
+
vec3 n;
|
|
228
|
+
float t = boxHit(lro, lrd, geos[k].w, n);
|
|
229
|
+
if (t > 0.0 && t < tMin) {
|
|
230
|
+
tMin = t;
|
|
231
|
+
hitK = k;
|
|
232
|
+
hitN = n;
|
|
233
|
+
hitP = lro + lrd * t;
|
|
234
|
+
hitInv = invs[k];
|
|
235
|
+
hitSize = geos[k].w;
|
|
236
|
+
hitSeed = seeds[k];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
bool hit = hitK >= 0;
|
|
241
|
+
float tFloor = (FLOOR_Z - ro.z) / rd.z;
|
|
242
|
+
float tHit = hit ? tMin : tFloor;
|
|
243
|
+
vec3 wp = ro + rd * tHit;
|
|
244
|
+
|
|
245
|
+
// 立方体法线:棱边一圈向外倾斜,形成圆润的倒角;每块顶面极轻微错开,像手工铺贴
|
|
246
|
+
vec3 n = vec3(0.0, 0.0, 1.0);
|
|
247
|
+
float bevel = 0.0;
|
|
248
|
+
if (hit) {
|
|
249
|
+
vec3 b = (1.0 - smoothstep(0.0, BEVEL, hitSize - abs(hitP))) * (1.0 - abs(hitN));
|
|
250
|
+
bevel = max(max(b.x, b.y), b.z);
|
|
251
|
+
vec3 jitter = (vec3(hitSeed, fract(hitSeed * 7.13), fract(hitSeed * 3.71)) - 0.5) * 0.04 * (1.0 - abs(hitN));
|
|
252
|
+
n = normalize(transposeM(hitInv) * normalize(hitN + sign(hitP) * b * 1.2 + jitter));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 主光源的投影:向光源方向检测其余立方体,命中即提前退出
|
|
256
|
+
float shadow = 1.0;
|
|
257
|
+
vec3 sp = wp + n * 0.003;
|
|
258
|
+
for (int k = 0; k < 9; k++) {
|
|
259
|
+
if (k == hitK) continue;
|
|
260
|
+
vec3 tmp;
|
|
261
|
+
float ts = boxHit(invs[k] * (sp - geos[k].xyz), localL[k], geos[k].w, tmp);
|
|
262
|
+
if (ts > 0.0) {
|
|
263
|
+
shadow = 0.0;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
vec3 V = -rd;
|
|
269
|
+
float diffK = max(dot(n, L), 0.0) * shadow;
|
|
270
|
+
float NdHk = max(dot(n, normalize(L + V)), 0.0);
|
|
271
|
+
|
|
272
|
+
vec3 color;
|
|
273
|
+
if (hit) {
|
|
274
|
+
float fres = pow(1.0 - max(dot(n, V), 0.0), 5.0);
|
|
275
|
+
vec3 env = envLight(reflect(rd, n));
|
|
276
|
+
|
|
277
|
+
vec3 edge = uHighlight * (0.03 + 0.4 * diffK)
|
|
278
|
+
+ mix(uHighlight, vec3(1.0), 0.3 + 0.7 * fres) * env * 1.4
|
|
279
|
+
+ specCol * pow(NdHk, 48.0) * shadow * 0.8;
|
|
280
|
+
|
|
281
|
+
vec3 face;
|
|
282
|
+
if (abs(hitN.z) > 0.5) {
|
|
283
|
+
face = uFront * (amb + 0.55 * diffK)
|
|
284
|
+
+ env * (0.12 + 0.88 * fres) * mix(1.0, 0.45, uScheme)
|
|
285
|
+
+ specCol * pow(NdHk, 900.0) * shadow * 0.4;
|
|
286
|
+
} else {
|
|
287
|
+
face = uColor * (amb * 0.6 + 0.02 + 0.7 * diffK)
|
|
288
|
+
+ mix(uColor, vec3(1.0), fres) * env * 0.8
|
|
289
|
+
+ mix(uColor, specCol, 0.5) * (pow(NdHk, 40.0) * shadow * 0.5);
|
|
290
|
+
}
|
|
291
|
+
color = mix(face, edge, bevel * 0.5);
|
|
292
|
+
} else {
|
|
293
|
+
// 槽底:紧贴立方体处压暗,滚动中的立方体下方透出强调色辉光
|
|
294
|
+
float ao = 1.0;
|
|
295
|
+
float glow = 0.0;
|
|
296
|
+
for (int k = 0; k < 9; k++) {
|
|
297
|
+
vec2 dv = wp.xy - geos[k].xy;
|
|
298
|
+
float d2 = dot(dv, dv);
|
|
299
|
+
// 超过此距离 ao 已饱和、辉光已衰减到看不见,跳过以省算力
|
|
300
|
+
if (d2 > 1.5) continue;
|
|
301
|
+
vec2 q = abs(dv) - geos[k].w;
|
|
302
|
+
ao *= mix(0.2, 1.0, smoothstep(0.0, 0.3, length(max(q, 0.0))));
|
|
303
|
+
glow += opens[k] * exp(-d2 / 0.18);
|
|
304
|
+
}
|
|
305
|
+
color = uFloor * (amb + 0.45 * diffK) * ao
|
|
306
|
+
+ uColor * (glow * 0.5 * (0.4 + 0.6 * ao));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 远处渐隐进雾里,拉开纵深
|
|
310
|
+
float fog = smoothstep(D * 0.95, D * 1.5, tHit);
|
|
311
|
+
color = mix(color, uFloor * mix(0.4, 0.9, uScheme), fog * 0.8);
|
|
312
|
+
|
|
313
|
+
acc += aces(color * mix(1.3, 1.05, uScheme));
|
|
314
|
+
}
|
|
315
|
+
vec3 color = acc / uSamples;
|
|
316
|
+
|
|
317
|
+
vec2 vc = frag / uResolution - 0.5;
|
|
318
|
+
color *= 1.0 - dot(vc, vc) * mix(1.1, 0.3, uScheme);
|
|
319
|
+
color = pow(color, vec3(1.0 / 2.2));
|
|
320
|
+
color += (hash(frag + fract(uTime)) - 0.5) / 255.0;
|
|
321
|
+
|
|
322
|
+
gl_FragColor = vec4(color, 1.0);
|
|
323
|
+
}
|
|
324
|
+
`;
|
|
325
|
+
|
|
326
|
+
export function FlipTiles({
|
|
327
|
+
children,
|
|
328
|
+
className,
|
|
329
|
+
color = '#4f6bff',
|
|
330
|
+
highlightColor = '#b8c4ff',
|
|
331
|
+
tileSize = 72,
|
|
332
|
+
speed = 1,
|
|
333
|
+
pattern = 'wave',
|
|
334
|
+
interactive = true,
|
|
335
|
+
}: FlipTilesProps) {
|
|
336
|
+
const scheme = useColorScheme();
|
|
337
|
+
const canvasRef = useShaderCanvas({
|
|
338
|
+
fragmentShader: FRAGMENT_SHADER,
|
|
339
|
+
speed,
|
|
340
|
+
interactive,
|
|
341
|
+
// 每像素要跑 9 次立方体求交与阴影射线,属于重填充率着色器:内部按 1.5× 渲染,
|
|
342
|
+
// 视觉上几乎无损,但相比 2× 能省下约 44% 的像素量
|
|
343
|
+
maxDpr: 1.5,
|
|
344
|
+
onDraw: ({ gl, uniform, dpr }) => {
|
|
345
|
+
const light = scheme === 'light';
|
|
346
|
+
gl.uniform1f(uniform('uScale'), Math.max(24, tileSize) * dpr);
|
|
347
|
+
gl.uniform3fv(uniform('uColor'), hexToLinearRgb(color));
|
|
348
|
+
gl.uniform3fv(uniform('uHighlight'), hexToLinearRgb(highlightColor));
|
|
349
|
+
gl.uniform3fv(uniform('uFront'), hexToLinearRgb(light ? '#ececf1' : '#0c0c12'));
|
|
350
|
+
gl.uniform3fv(uniform('uFloor'), hexToLinearRgb(light ? '#c6c6d0' : '#040406'));
|
|
351
|
+
gl.uniform1f(uniform('uPattern'), Math.max(0, PATTERNS.indexOf(pattern)));
|
|
352
|
+
gl.uniform1f(uniform('uScheme'), light ? 1 : 0);
|
|
353
|
+
// 采样偏移只在单个像素内抖动,两次采样已足够压住立方体棱边的锯齿
|
|
354
|
+
gl.uniform1f(uniform('uSamples'), 2);
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<div
|
|
360
|
+
className={cn('elyri-flip-tiles', className)}
|
|
361
|
+
data-scheme={scheme}
|
|
362
|
+
style={{ '--elyri-flip-tiles-color': color } as CSSProperties}
|
|
363
|
+
>
|
|
364
|
+
<canvas ref={canvasRef} className="elyri-flip-tiles__canvas" aria-hidden="true" />
|
|
365
|
+
{children != null && <div className="elyri-flip-tiles__content">{children}</div>}
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
.elyri-halo {
|
|
2
|
+
--elyri-halo-color: #8fb4ff;
|
|
3
|
+
--elyri-halo-bg: #010102;
|
|
4
|
+
|
|
5
|
+
position: relative;
|
|
6
|
+
isolation: isolate;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
/* WebGL 不可用或尚未绘制时的底色 */
|
|
9
|
+
background:
|
|
10
|
+
radial-gradient(
|
|
11
|
+
circle at 50% 50%,
|
|
12
|
+
transparent 27vmin,
|
|
13
|
+
color-mix(in srgb, var(--elyri-halo-color) 22%, transparent) 30vmin,
|
|
14
|
+
transparent 33vmin
|
|
15
|
+
),
|
|
16
|
+
var(--elyri-halo-bg);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.elyri-halo[data-scheme='light'] {
|
|
20
|
+
--elyri-halo-bg: #f8f8fa;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.elyri-halo__canvas {
|
|
24
|
+
position: absolute;
|
|
25
|
+
inset: 0;
|
|
26
|
+
z-index: 0;
|
|
27
|
+
display: block;
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: 100%;
|
|
30
|
+
opacity: 0;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
transition: opacity var(--elyri-duration-slow) var(--elyri-ease-out);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.elyri-halo__canvas[data-ready] {
|
|
36
|
+
opacity: 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.elyri-halo__content {
|
|
40
|
+
position: relative;
|
|
41
|
+
z-index: 1;
|
|
42
|
+
height: 100%;
|
|
43
|
+
}
|