@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,122 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn, hexToLinearRgb, useColorScheme, useShaderCanvas } from '../../../core';
|
|
4
|
+
|
|
5
|
+
import './Halo.css';
|
|
6
|
+
|
|
7
|
+
export interface HaloProps {
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
/** 光环的两种渐变色(hex),沿圆周缓慢流转 */
|
|
11
|
+
colors?: [string, string];
|
|
12
|
+
/** 光环半径,相对画面高度的倍率 */
|
|
13
|
+
size?: number;
|
|
14
|
+
/** 动画速度倍率 */
|
|
15
|
+
speed?: number;
|
|
16
|
+
/** 光环中心是否轻微偏向指针 */
|
|
17
|
+
interactive?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_COLORS: [string, string] = ['#8fb4ff', '#e7c6ff'];
|
|
21
|
+
|
|
22
|
+
// 画面中央一圈极细的光环:半径随角度轻微起伏,两束高光沿圆周流动,
|
|
23
|
+
// 外侧只有很淡的辉光,其余区域保持近黑,留白克制。
|
|
24
|
+
const FRAGMENT_SHADER = `
|
|
25
|
+
precision highp float;
|
|
26
|
+
|
|
27
|
+
uniform vec2 uResolution;
|
|
28
|
+
uniform float uTime;
|
|
29
|
+
uniform vec2 uPointer;
|
|
30
|
+
uniform vec3 uColor1;
|
|
31
|
+
uniform vec3 uColor2;
|
|
32
|
+
uniform float uSize;
|
|
33
|
+
uniform float uInteractive;
|
|
34
|
+
uniform vec3 uBg;
|
|
35
|
+
uniform float uScheme;
|
|
36
|
+
|
|
37
|
+
float hash(vec2 p) {
|
|
38
|
+
p = fract(p * vec2(123.34, 456.21));
|
|
39
|
+
p += dot(p, p + 45.32);
|
|
40
|
+
return fract(p.x * p.y);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 深色下光线叠加发亮;浅色下改为按色相吸收底色,像墨色细线落在纸面上
|
|
44
|
+
vec3 shade(vec3 bg, vec3 light) {
|
|
45
|
+
if (uScheme > 0.5) {
|
|
46
|
+
float amount = max(max(light.r, light.g), light.b);
|
|
47
|
+
vec3 tint = light / max(amount, 1e-4);
|
|
48
|
+
return bg * exp(-amount * 1.6 * (1.0 - tint * 0.8));
|
|
49
|
+
}
|
|
50
|
+
return bg + 1.0 - exp(-light * 1.4);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
void main() {
|
|
54
|
+
vec2 res = uResolution;
|
|
55
|
+
vec2 p = (gl_FragCoord.xy - 0.5 * res) / res.y;
|
|
56
|
+
vec2 pointer = (vec2(uPointer.x, res.y - uPointer.y) - 0.5 * res) / res.y;
|
|
57
|
+
p -= pointer * 0.06 * uInteractive;
|
|
58
|
+
float t = uTime;
|
|
59
|
+
|
|
60
|
+
float r = length(p);
|
|
61
|
+
float a = atan(p.y, p.x);
|
|
62
|
+
|
|
63
|
+
float radius = 0.3 * uSize;
|
|
64
|
+
radius += 0.010 * sin(a * 3.0 + t * 0.5) + 0.006 * sin(a * 5.0 - t * 0.8);
|
|
65
|
+
float d = abs(r - radius);
|
|
66
|
+
|
|
67
|
+
float line = exp(-d * d * 60000.0);
|
|
68
|
+
float glow = exp(-d * 22.0) * 0.28 + exp(-d * 6.0) * 0.05;
|
|
69
|
+
|
|
70
|
+
// 两束高光沿圆周以不同速度流动
|
|
71
|
+
float f1 = pow(0.5 + 0.5 * cos(a - t * 0.45), 6.0);
|
|
72
|
+
float f2 = pow(0.5 + 0.5 * cos(a + 2.4 + t * 0.28), 10.0);
|
|
73
|
+
float flow = 0.12 + f1 + 0.6 * f2;
|
|
74
|
+
|
|
75
|
+
vec3 tint = mix(uColor1, uColor2, 0.5 + 0.5 * sin(a + t * 0.2));
|
|
76
|
+
vec3 light = tint * (line * 1.1 + glow) * flow;
|
|
77
|
+
// 圈内极淡的雾光
|
|
78
|
+
light += uColor1 * 0.015 * exp(-r * r / (radius * radius) * 1.5);
|
|
79
|
+
|
|
80
|
+
vec3 color = pow(clamp(shade(uBg, light), 0.0, 1.0), vec3(1.0 / 2.2));
|
|
81
|
+
color += (hash(gl_FragCoord.xy + fract(t)) - 0.5) / 255.0;
|
|
82
|
+
|
|
83
|
+
gl_FragColor = vec4(color, 1.0);
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
export function Halo({
|
|
88
|
+
children,
|
|
89
|
+
className,
|
|
90
|
+
colors = DEFAULT_COLORS,
|
|
91
|
+
size = 1,
|
|
92
|
+
speed = 1,
|
|
93
|
+
interactive = true,
|
|
94
|
+
}: HaloProps) {
|
|
95
|
+
const [first = DEFAULT_COLORS[0], second = DEFAULT_COLORS[1]] = colors;
|
|
96
|
+
const scheme = useColorScheme();
|
|
97
|
+
|
|
98
|
+
const canvasRef = useShaderCanvas({
|
|
99
|
+
fragmentShader: FRAGMENT_SHADER,
|
|
100
|
+
speed,
|
|
101
|
+
interactive,
|
|
102
|
+
onDraw: ({ gl, uniform }) => {
|
|
103
|
+
gl.uniform3fv(uniform('uColor1'), hexToLinearRgb(first));
|
|
104
|
+
gl.uniform3fv(uniform('uColor2'), hexToLinearRgb(second));
|
|
105
|
+
gl.uniform1f(uniform('uSize'), Math.max(0.1, size));
|
|
106
|
+
gl.uniform1f(uniform('uInteractive'), interactive ? 1 : 0);
|
|
107
|
+
gl.uniform3fv(uniform('uBg'), hexToLinearRgb(scheme === 'light' ? '#f8f8fa' : '#010102'));
|
|
108
|
+
gl.uniform1f(uniform('uScheme'), scheme === 'light' ? 1 : 0);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
className={cn('elyri-halo', className)}
|
|
115
|
+
data-scheme={scheme}
|
|
116
|
+
style={{ '--elyri-halo-color': first } as CSSProperties}
|
|
117
|
+
>
|
|
118
|
+
<canvas ref={canvasRef} className="elyri-halo__canvas" aria-hidden="true" />
|
|
119
|
+
{children != null && <div className="elyri-halo__content">{children}</div>}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
.elyri-mesh-gradient {
|
|
2
|
+
--elyri-mesh-gradient-c0: #6e6af0;
|
|
3
|
+
--elyri-mesh-gradient-c1: #f472b6;
|
|
4
|
+
--elyri-mesh-gradient-c2: #fbbf77;
|
|
5
|
+
--elyri-mesh-gradient-c3: #38bdf8;
|
|
6
|
+
--elyri-mesh-gradient-base: #0b0b12;
|
|
7
|
+
|
|
8
|
+
position: relative;
|
|
9
|
+
isolation: isolate;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
/* WebGL 不可用或尚未绘制时的底色:四角色团的静态近似 */
|
|
12
|
+
background:
|
|
13
|
+
radial-gradient(60% 70% at 18% 22%, var(--elyri-mesh-gradient-c0), transparent 70%),
|
|
14
|
+
radial-gradient(60% 70% at 82% 28%, var(--elyri-mesh-gradient-c1), transparent 70%),
|
|
15
|
+
radial-gradient(60% 70% at 78% 82%, var(--elyri-mesh-gradient-c2), transparent 70%),
|
|
16
|
+
radial-gradient(60% 70% at 22% 78%, var(--elyri-mesh-gradient-c3), transparent 70%), var(--elyri-mesh-gradient-base);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.elyri-mesh-gradient[data-scheme='light'] {
|
|
20
|
+
--elyri-mesh-gradient-base: #f7f7fa;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.elyri-mesh-gradient__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-mesh-gradient__canvas[data-ready] {
|
|
36
|
+
opacity: 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.elyri-mesh-gradient__content {
|
|
40
|
+
position: relative;
|
|
41
|
+
z-index: 1;
|
|
42
|
+
height: 100%;
|
|
43
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn, hexToLinearRgb, useColorScheme, useShaderCanvas } from '../../../core';
|
|
4
|
+
|
|
5
|
+
import './MeshGradient.css';
|
|
6
|
+
|
|
7
|
+
export interface MeshGradientProps {
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
/** 自定义内联样式,可用于控制尺寸 */
|
|
11
|
+
style?: CSSProperties;
|
|
12
|
+
/** 四个色团的颜色(hex) */
|
|
13
|
+
colors?: [string, string, string, string];
|
|
14
|
+
/** 胶片颗粒强度,0 为关闭 */
|
|
15
|
+
grain?: number;
|
|
16
|
+
/** 动画速度倍率 */
|
|
17
|
+
speed?: number;
|
|
18
|
+
/** 指针处的色彩漩涡是否跟随指针 */
|
|
19
|
+
interactive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_COLORS: [string, string, string, string] = ['#6e6af0', '#f472b6', '#fbbf77', '#38bdf8'];
|
|
23
|
+
|
|
24
|
+
// 流光渐变:四个色团沿互不相关的缓慢轨迹漂移,定位坐标先经过两级
|
|
25
|
+
// 分形噪声域扭曲(q → r),色团边界被拉成缓缓流淌、相互卷入的彩色丝带;
|
|
26
|
+
// 颜色在 Oklab 空间混合(过渡不发灰)。沿流场等值线叠一层缎面光泽,
|
|
27
|
+
// 暗部轻微压低,形成丝绸般的起伏与流光。指针处色彩被轻轻卷成漩涡。
|
|
28
|
+
const FRAGMENT_SHADER = `
|
|
29
|
+
precision highp float;
|
|
30
|
+
|
|
31
|
+
uniform vec2 uResolution;
|
|
32
|
+
uniform float uTime;
|
|
33
|
+
uniform vec2 uPointer;
|
|
34
|
+
uniform vec3 uColor0;
|
|
35
|
+
uniform vec3 uColor1;
|
|
36
|
+
uniform vec3 uColor2;
|
|
37
|
+
uniform vec3 uColor3;
|
|
38
|
+
uniform vec3 uBase;
|
|
39
|
+
uniform float uGrain;
|
|
40
|
+
|
|
41
|
+
float hash(vec2 p) {
|
|
42
|
+
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
|
43
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
44
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
vec3 toOklab(vec3 c) {
|
|
48
|
+
float l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b;
|
|
49
|
+
float m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b;
|
|
50
|
+
float s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b;
|
|
51
|
+
l = pow(max(l, 0.0), 1.0 / 3.0);
|
|
52
|
+
m = pow(max(m, 0.0), 1.0 / 3.0);
|
|
53
|
+
s = pow(max(s, 0.0), 1.0 / 3.0);
|
|
54
|
+
return vec3(
|
|
55
|
+
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
56
|
+
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
57
|
+
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
vec3 fromOklab(vec3 c) {
|
|
62
|
+
float l = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z;
|
|
63
|
+
float m = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z;
|
|
64
|
+
float s = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z;
|
|
65
|
+
l = l * l * l;
|
|
66
|
+
m = m * m * m;
|
|
67
|
+
s = s * s * s;
|
|
68
|
+
return vec3(
|
|
69
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
70
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
71
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
float noise(vec2 p) {
|
|
76
|
+
vec2 i = floor(p);
|
|
77
|
+
vec2 f = fract(p);
|
|
78
|
+
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
|
79
|
+
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);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
float fbm(vec2 p) {
|
|
83
|
+
float v = 0.0;
|
|
84
|
+
float a = 0.5;
|
|
85
|
+
mat2 rot = mat2(1.6, 1.2, -1.2, 1.6);
|
|
86
|
+
for (int i = 0; i < 4; i++) {
|
|
87
|
+
v += a * noise(p);
|
|
88
|
+
p = rot * p + vec2(11.3, 7.9);
|
|
89
|
+
a *= 0.5;
|
|
90
|
+
}
|
|
91
|
+
return v;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
float blob(vec2 uv, vec2 center) {
|
|
95
|
+
vec2 d = uv - center;
|
|
96
|
+
return exp(-dot(d, d) * 3.2);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
void main() {
|
|
100
|
+
vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y);
|
|
101
|
+
float minRes = min(uResolution.x, uResolution.y);
|
|
102
|
+
vec2 half_ = 0.5 * uResolution / minRes;
|
|
103
|
+
vec2 uv = (frag - 0.5 * uResolution) / minRes;
|
|
104
|
+
vec2 pointer = (uPointer - 0.5 * uResolution) / minRes;
|
|
105
|
+
float t = uTime * 0.1;
|
|
106
|
+
|
|
107
|
+
// 指针漩涡:绕指针旋转采样坐标,越靠近转得越多,色彩被卷进去
|
|
108
|
+
vec2 dm = uv - pointer;
|
|
109
|
+
float swirl = exp(-dot(dm, dm) / 0.07);
|
|
110
|
+
float angle = swirl * 1.1;
|
|
111
|
+
uv = pointer + mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * dm * (1.0 - swirl * 0.12);
|
|
112
|
+
|
|
113
|
+
// 两级域扭曲的流场:q 缓慢漂移,r 在 q 的基础上再扭一次,得到液体般的卷流
|
|
114
|
+
vec2 p = uv * 1.5;
|
|
115
|
+
vec2 q = vec2(fbm(p + vec2(0.0, t)), fbm(p + vec2(5.2, 1.3) - t * 0.8));
|
|
116
|
+
vec2 r = vec2(fbm(p + 2.6 * q + vec2(1.7, 9.2) + t * 1.2), fbm(p + 2.6 * q + vec2(8.3, 2.8) - t));
|
|
117
|
+
vec2 w = uv + (r - 0.5) * 0.9 + (q - 0.5) * 0.3;
|
|
118
|
+
|
|
119
|
+
float tb = uTime * 0.12;
|
|
120
|
+
vec2 p0 = half_ * vec2(-0.55 + 0.35 * sin(tb * 0.63), -0.45 + 0.35 * cos(tb * 0.51));
|
|
121
|
+
vec2 p1 = half_ * vec2(0.55 + 0.30 * cos(tb * 0.57 + 1.0), -0.35 + 0.40 * sin(tb * 0.71 + 2.0));
|
|
122
|
+
vec2 p2 = half_ * vec2(0.45 + 0.35 * sin(tb * 0.49 + 3.0), 0.50 + 0.30 * cos(tb * 0.61 + 1.5));
|
|
123
|
+
vec2 p3 = half_ * vec2(-0.50 + 0.35 * cos(tb * 0.67 + 4.0), 0.45 + 0.35 * sin(tb * 0.53 + 0.5));
|
|
124
|
+
|
|
125
|
+
float w0 = blob(w, p0);
|
|
126
|
+
float w1 = blob(w, p1);
|
|
127
|
+
float w2 = blob(w, p2);
|
|
128
|
+
float w3 = blob(w, p3);
|
|
129
|
+
float wb = 0.05;
|
|
130
|
+
|
|
131
|
+
vec3 lab = (toOklab(uColor0) * w0 + toOklab(uColor1) * w1 + toOklab(uColor2) * w2 + toOklab(uColor3) * w3 + toOklab(uBase) * wb)
|
|
132
|
+
/ (w0 + w1 + w2 + w3 + wb);
|
|
133
|
+
|
|
134
|
+
// 缎面流光:沿流场等值线的柔和亮带随时间滑过,谷底微暗,带出丝绸般的层次
|
|
135
|
+
float band = sin((r.x * 1.4 - r.y + q.x * 0.6) * 11.0 + t * 3.0);
|
|
136
|
+
float gloss = smoothstep(0.15, 1.0, band);
|
|
137
|
+
lab.x += gloss * gloss * 0.06 - (1.0 - smoothstep(-1.0, 0.1, band)) * 0.025;
|
|
138
|
+
lab.x += swirl * 0.025;
|
|
139
|
+
// 高光处略降饱和,像光打在织物表面
|
|
140
|
+
lab.yz *= 1.0 - gloss * gloss * 0.12;
|
|
141
|
+
|
|
142
|
+
vec3 color = pow(clamp(fromOklab(lab), 0.0, 1.0), vec3(1.0 / 2.2));
|
|
143
|
+
|
|
144
|
+
// 胶片颗粒 + 抖动,消除大面积渐变的色带
|
|
145
|
+
float n = hash(frag + fract(uTime * 7.31) * 113.0) - 0.5;
|
|
146
|
+
color += n * (uGrain * 0.07 + 1.0 / 255.0);
|
|
147
|
+
|
|
148
|
+
gl_FragColor = vec4(color, 1.0);
|
|
149
|
+
}
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
export function MeshGradient({
|
|
153
|
+
children,
|
|
154
|
+
className,
|
|
155
|
+
style,
|
|
156
|
+
colors = DEFAULT_COLORS,
|
|
157
|
+
grain = 0.35,
|
|
158
|
+
speed = 1,
|
|
159
|
+
interactive = true,
|
|
160
|
+
}: MeshGradientProps) {
|
|
161
|
+
const scheme = useColorScheme();
|
|
162
|
+
const [c0 = DEFAULT_COLORS[0], c1 = DEFAULT_COLORS[1], c2 = DEFAULT_COLORS[2], c3 = DEFAULT_COLORS[3]] = colors;
|
|
163
|
+
const canvasRef = useShaderCanvas({
|
|
164
|
+
fragmentShader: FRAGMENT_SHADER,
|
|
165
|
+
speed,
|
|
166
|
+
interactive,
|
|
167
|
+
onDraw: ({ gl, uniform }) => {
|
|
168
|
+
gl.uniform3fv(uniform('uColor0'), hexToLinearRgb(c0));
|
|
169
|
+
gl.uniform3fv(uniform('uColor1'), hexToLinearRgb(c1));
|
|
170
|
+
gl.uniform3fv(uniform('uColor2'), hexToLinearRgb(c2));
|
|
171
|
+
gl.uniform3fv(uniform('uColor3'), hexToLinearRgb(c3));
|
|
172
|
+
gl.uniform3fv(uniform('uBase'), hexToLinearRgb(scheme === 'light' ? '#f7f7fa' : '#0b0b12'));
|
|
173
|
+
gl.uniform1f(uniform('uGrain'), Math.max(0, grain));
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
className={cn('elyri-mesh-gradient', className)}
|
|
180
|
+
data-scheme={scheme}
|
|
181
|
+
style={
|
|
182
|
+
{
|
|
183
|
+
'--elyri-mesh-gradient-c0': c0,
|
|
184
|
+
'--elyri-mesh-gradient-c1': c1,
|
|
185
|
+
'--elyri-mesh-gradient-c2': c2,
|
|
186
|
+
'--elyri-mesh-gradient-c3': c3,
|
|
187
|
+
...style,
|
|
188
|
+
} as CSSProperties
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
<canvas ref={canvasRef} className="elyri-mesh-gradient__canvas" aria-hidden="true" />
|
|
192
|
+
{children != null && <div className="elyri-mesh-gradient__content">{children}</div>}
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.elyri-pixel-vortex {
|
|
2
|
+
--elyri-pixel-vortex-color: #ff4d12;
|
|
3
|
+
|
|
4
|
+
position: relative;
|
|
5
|
+
isolation: isolate;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
/* WebGL 不可用或尚未绘制时的底色 */
|
|
8
|
+
background: radial-gradient(
|
|
9
|
+
circle at 50% 45%,
|
|
10
|
+
color-mix(in srgb, var(--elyri-pixel-vortex-color) 45%, #000),
|
|
11
|
+
#0a0706 70%
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* 浅色主题:兜底底色同步翻转为浅色,避免就绪前闪深色 */
|
|
16
|
+
.elyri-pixel-vortex[data-scheme='light'] {
|
|
17
|
+
background: radial-gradient(
|
|
18
|
+
circle at 50% 45%,
|
|
19
|
+
color-mix(in srgb, var(--elyri-pixel-vortex-color) 18%, #ffffff),
|
|
20
|
+
#f6f5f4 70%
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.elyri-pixel-vortex__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-pixel-vortex__canvas[data-ready] {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.elyri-pixel-vortex__content {
|
|
41
|
+
position: relative;
|
|
42
|
+
z-index: 1;
|
|
43
|
+
height: 100%;
|
|
44
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn, hexToLinearRgb, useColorScheme, useShaderCanvas } from '../../../core';
|
|
5
|
+
|
|
6
|
+
import './PixelVortex.css';
|
|
7
|
+
|
|
8
|
+
export interface PixelVortexProps {
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
/**
|
|
12
|
+
* 在 LED 屏上播放的视频地址。跨域视频需服务端允许 CORS;服务端还需支持 Range 请求,
|
|
13
|
+
* 否则大视频会整段下载而无法边下边播。未提供或加载失败时显示火焰流动效果。
|
|
14
|
+
*/
|
|
15
|
+
videoSrc?: string;
|
|
16
|
+
/** 火焰主色(hex) */
|
|
17
|
+
color?: string;
|
|
18
|
+
/** 最亮处的高光色(hex) */
|
|
19
|
+
highlightColor?: string;
|
|
20
|
+
/** 单颗灯珠尺寸(CSS 像素) */
|
|
21
|
+
cellSize?: number;
|
|
22
|
+
/** 是否显示拼接屏接缝(每 16×10 颗灯珠一块) */
|
|
23
|
+
seams?: boolean;
|
|
24
|
+
/** 接缝宽度(CSS 像素) */
|
|
25
|
+
seamWidth?: number;
|
|
26
|
+
/** 动画速度倍率 */
|
|
27
|
+
speed?: number;
|
|
28
|
+
/** 光晕中心是否跟随指针 */
|
|
29
|
+
interactive?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// LED 屏:按灯珠网格对画面降采样,每颗灯珠带亮度抖动与色阶量化,再叠加灯珠间隙与可选的拼接缝。
|
|
33
|
+
// 画面来源为视频(cover 方式铺满),没有视频时为缓慢流动的火焰,亮区跟随指针;浅色主题下底板为浅色。
|
|
34
|
+
const FRAGMENT_SHADER = `
|
|
35
|
+
precision highp float;
|
|
36
|
+
|
|
37
|
+
uniform vec2 uResolution;
|
|
38
|
+
uniform float uTime;
|
|
39
|
+
uniform vec2 uPointer;
|
|
40
|
+
uniform vec3 uColor;
|
|
41
|
+
uniform vec3 uHighlight;
|
|
42
|
+
uniform float uCell;
|
|
43
|
+
uniform float uFollow;
|
|
44
|
+
uniform sampler2D uVideo;
|
|
45
|
+
uniform float uHasVideo;
|
|
46
|
+
uniform vec2 uVideoSize;
|
|
47
|
+
uniform float uScheme;
|
|
48
|
+
uniform float uSeam;
|
|
49
|
+
|
|
50
|
+
float hash(vec2 p) {
|
|
51
|
+
p = fract(p * vec2(123.34, 456.21));
|
|
52
|
+
p += dot(p, p + 45.32);
|
|
53
|
+
return fract(p.x * p.y);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
float noise(vec2 p) {
|
|
57
|
+
vec2 i = floor(p);
|
|
58
|
+
vec2 f = fract(p);
|
|
59
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
60
|
+
return mix(
|
|
61
|
+
mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),
|
|
62
|
+
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),
|
|
63
|
+
u.y
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
float fbm(vec2 p) {
|
|
68
|
+
float v = 0.0;
|
|
69
|
+
float a = 0.5;
|
|
70
|
+
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
|
|
71
|
+
for (int i = 0; i < 5; i++) {
|
|
72
|
+
v += a * noise(p);
|
|
73
|
+
p = m * p;
|
|
74
|
+
a *= 0.5;
|
|
75
|
+
}
|
|
76
|
+
return v;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 火焰热度场:域扭曲的噪声缓慢上升,靠近焦点处更亮
|
|
80
|
+
float heat(vec2 p, vec2 focus, float t) {
|
|
81
|
+
vec2 q = vec2(
|
|
82
|
+
fbm(p * 1.4 + vec2(0.0, t * 0.18)),
|
|
83
|
+
fbm(p * 1.4 + vec2(5.2, 1.3) - t * 0.12)
|
|
84
|
+
);
|
|
85
|
+
float n = fbm(p * 2.0 + q * 1.7 + vec2(0.0, t * 0.3));
|
|
86
|
+
vec2 d = p - focus;
|
|
87
|
+
float glow = exp(-dot(d, d) * 1.6);
|
|
88
|
+
return n * n * 1.8 * (0.3 + 0.9 * glow);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
vec3 aces(vec3 x) {
|
|
92
|
+
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
void main() {
|
|
96
|
+
vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y);
|
|
97
|
+
float cell = max(uCell, 2.0);
|
|
98
|
+
float minRes = min(uResolution.x, uResolution.y);
|
|
99
|
+
float t = uTime;
|
|
100
|
+
|
|
101
|
+
// 以灯珠中心采样,整颗灯珠同色
|
|
102
|
+
vec2 id = floor(frag / cell);
|
|
103
|
+
vec2 center = (id + 0.5) * cell;
|
|
104
|
+
vec2 vc = frag / uResolution - 0.5;
|
|
105
|
+
float grain = (0.86 + 0.2 * hash(id)) * (0.95 + 0.1 * hash(id + floor(t * 10.0) * vec2(7.13, 3.71)));
|
|
106
|
+
vec3 color;
|
|
107
|
+
|
|
108
|
+
if (uHasVideo > 0.5) {
|
|
109
|
+
// cover 铺满:按宽高比裁切视频
|
|
110
|
+
vec2 uv = center / uResolution - 0.5;
|
|
111
|
+
float screenAspect = uResolution.x / uResolution.y;
|
|
112
|
+
float videoAspect = uVideoSize.x / max(uVideoSize.y, 1.0);
|
|
113
|
+
if (screenAspect > videoAspect) uv.y *= videoAspect / screenAspect;
|
|
114
|
+
else uv.x *= screenAspect / videoAspect;
|
|
115
|
+
color = texture2D(uVideo, uv + 0.5).rgb * grain * 1.1;
|
|
116
|
+
// 深色压暗四角,浅色提亮四角
|
|
117
|
+
color = mix(color * (1.0 - dot(vc, vc) * 0.6), mix(color, vec3(1.0), dot(vc, vc) * 0.8), uScheme);
|
|
118
|
+
} else {
|
|
119
|
+
vec2 p = (center - 0.5 * uResolution) / minRes;
|
|
120
|
+
vec2 focus = mix(vec2(0.0, 0.1), (uPointer - 0.5 * uResolution) / minRes, 0.5 * uFollow);
|
|
121
|
+
float h = heat(p, focus, t) * grain;
|
|
122
|
+
color = uColor * h * 1.3 + uHighlight * pow(max(h - 0.55, 0.0), 2.0) * 2.2;
|
|
123
|
+
color += uColor * 0.012;
|
|
124
|
+
color *= 1.0 - dot(vc, vc) * 0.9;
|
|
125
|
+
color = pow(aces(color), vec3(1.0 / 2.2));
|
|
126
|
+
if (uScheme > 0.5) {
|
|
127
|
+
// 浅色:亮暗镜像并保留色相 —— 深底亮焰翻转为浅底深焰
|
|
128
|
+
float lum = dot(color, vec3(0.2126, 0.7152, 0.0722));
|
|
129
|
+
color = clamp(color + (1.0 - 2.0 * lum), 0.0, 1.0);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 色阶量化,模拟 LED 驱动的有限灰度
|
|
134
|
+
color = floor(color * 14.0 + hash(id + 3.1)) / 14.0;
|
|
135
|
+
|
|
136
|
+
// 灯珠形状:方形发光点;间隙深色时保留少许漏光,浅色时为浅灰底板
|
|
137
|
+
vec2 f = fract(frag / cell) - 0.5;
|
|
138
|
+
float d = max(abs(f.x), abs(f.y));
|
|
139
|
+
vec3 gap = mix(color * 0.2, mix(color, vec3(0.96), 0.7), uScheme);
|
|
140
|
+
color = mix(gap, color, smoothstep(0.5, 0.28, d));
|
|
141
|
+
|
|
142
|
+
// 拼接屏接缝:每 16×10 颗灯珠一块,接缝 + 缝下方的阴影
|
|
143
|
+
if (uSeam > 0.0) {
|
|
144
|
+
vec2 pp = mod(frag, vec2(16.0, 10.0) * cell);
|
|
145
|
+
float seam = max(1.0 - smoothstep(uSeam - 1.0, uSeam + 1.0, pp.x), 1.0 - smoothstep(uSeam - 1.0, uSeam + 1.0, pp.y));
|
|
146
|
+
float shade = max(1.0 - smoothstep(uSeam, uSeam + cell * 1.5, pp.x), 1.0 - smoothstep(uSeam, uSeam + cell * 1.5, pp.y));
|
|
147
|
+
color *= 1.0 - mix(0.45, 0.15, uScheme) * shade;
|
|
148
|
+
vec3 seamColor = mix(vec3(0.018, 0.016, 0.015), vec3(0.86, 0.855, 0.85), uScheme) + color * 0.06;
|
|
149
|
+
color = mix(color, seamColor, seam);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
gl_FragColor = vec4(color, 1.0);
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
export function PixelVortex({
|
|
157
|
+
children,
|
|
158
|
+
className,
|
|
159
|
+
videoSrc,
|
|
160
|
+
color = '#ff4d12',
|
|
161
|
+
highlightColor = '#ffd9b0',
|
|
162
|
+
cellSize = 4,
|
|
163
|
+
seams = false,
|
|
164
|
+
seamWidth = 5,
|
|
165
|
+
speed = 1,
|
|
166
|
+
interactive = true,
|
|
167
|
+
}: PixelVortexProps) {
|
|
168
|
+
const scheme = useColorScheme();
|
|
169
|
+
const [video, setVideo] = useState<HTMLVideoElement | null>(null);
|
|
170
|
+
// 一旦出过帧就锁定为「有视频」:大视频中途缓冲时保留上一帧,不回退到火焰
|
|
171
|
+
const [videoReady, setVideoReady] = useState(false);
|
|
172
|
+
const textureRef = useRef<{ gl: WebGLRenderingContext; texture: WebGLTexture | null } | null>(null);
|
|
173
|
+
|
|
174
|
+
// 视频不挂到 DOM 中,仅作为纹理来源;可以播放后才切换到视频画面。
|
|
175
|
+
// 大视频依赖服务端支持 Range 请求做流式播放:边下边播,只缓冲播放位置附近的数据。
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!videoSrc) return;
|
|
178
|
+
const el = document.createElement('video');
|
|
179
|
+
el.crossOrigin = 'anonymous';
|
|
180
|
+
el.muted = true;
|
|
181
|
+
el.loop = true;
|
|
182
|
+
el.playsInline = true;
|
|
183
|
+
el.preload = 'auto';
|
|
184
|
+
|
|
185
|
+
let disposed = false;
|
|
186
|
+
const tryPlay = () => {
|
|
187
|
+
// 页面不可见时暂停,避免大视频在后台持续缓冲
|
|
188
|
+
if (disposed || document.hidden) return;
|
|
189
|
+
el.play().catch(() => {});
|
|
190
|
+
};
|
|
191
|
+
const handleReady = () => {
|
|
192
|
+
if (disposed) return;
|
|
193
|
+
setVideo(el);
|
|
194
|
+
setVideoReady(true);
|
|
195
|
+
};
|
|
196
|
+
const handleError = () => {
|
|
197
|
+
if (disposed) return;
|
|
198
|
+
setVideoReady(false);
|
|
199
|
+
setVideo(null);
|
|
200
|
+
};
|
|
201
|
+
// 大视频会周期性卡顿而暂停,恢复可播/可见时继续自动播放
|
|
202
|
+
const handlePause = () => tryPlay();
|
|
203
|
+
const handleVisibility = () => {
|
|
204
|
+
if (document.hidden) el.pause();
|
|
205
|
+
else tryPlay();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
el.addEventListener('loadeddata', handleReady);
|
|
209
|
+
el.addEventListener('canplay', handleReady);
|
|
210
|
+
el.addEventListener('error', handleError);
|
|
211
|
+
el.addEventListener('pause', handlePause);
|
|
212
|
+
document.addEventListener('visibilitychange', handleVisibility);
|
|
213
|
+
el.src = videoSrc;
|
|
214
|
+
tryPlay();
|
|
215
|
+
|
|
216
|
+
return () => {
|
|
217
|
+
disposed = true;
|
|
218
|
+
el.removeEventListener('loadeddata', handleReady);
|
|
219
|
+
el.removeEventListener('canplay', handleReady);
|
|
220
|
+
el.removeEventListener('error', handleError);
|
|
221
|
+
el.removeEventListener('pause', handlePause);
|
|
222
|
+
document.removeEventListener('visibilitychange', handleVisibility);
|
|
223
|
+
el.pause();
|
|
224
|
+
el.removeAttribute('src');
|
|
225
|
+
el.load();
|
|
226
|
+
setVideoReady(false);
|
|
227
|
+
setVideo(null);
|
|
228
|
+
};
|
|
229
|
+
}, [videoSrc]);
|
|
230
|
+
|
|
231
|
+
const canvasRef = useShaderCanvas({
|
|
232
|
+
fragmentShader: FRAGMENT_SHADER,
|
|
233
|
+
speed,
|
|
234
|
+
interactive,
|
|
235
|
+
onDraw: ({ gl, uniform, dpr }) => {
|
|
236
|
+
gl.uniform3fv(uniform('uColor'), hexToLinearRgb(color));
|
|
237
|
+
gl.uniform3fv(uniform('uHighlight'), hexToLinearRgb(highlightColor));
|
|
238
|
+
gl.uniform1f(uniform('uCell'), Math.max(2, cellSize) * dpr);
|
|
239
|
+
gl.uniform1f(uniform('uFollow'), interactive ? 1 : 0);
|
|
240
|
+
gl.uniform1f(uniform('uScheme'), scheme === 'light' ? 1 : 0);
|
|
241
|
+
gl.uniform1f(uniform('uSeam'), seams ? Math.max(0.5, seamWidth) * dpr : 0);
|
|
242
|
+
|
|
243
|
+
// 用锁定状态判断是否已切到视频;缓冲中 readyState 会掉,但画面保留上一帧
|
|
244
|
+
const hasVideo = video != null && videoReady;
|
|
245
|
+
gl.uniform1f(uniform('uHasVideo'), hasVideo ? 1 : 0);
|
|
246
|
+
if (!hasVideo || !video) return;
|
|
247
|
+
|
|
248
|
+
if (textureRef.current?.gl !== gl) {
|
|
249
|
+
const texture = gl.createTexture();
|
|
250
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
251
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
252
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
253
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
254
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
255
|
+
textureRef.current = { gl, texture };
|
|
256
|
+
}
|
|
257
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
258
|
+
gl.bindTexture(gl.TEXTURE_2D, textureRef.current.texture);
|
|
259
|
+
// 只在拿到当前帧时上传;缓冲中沿用上一帧纹理
|
|
260
|
+
if (video.readyState >= video.HAVE_CURRENT_DATA && video.videoWidth > 0) {
|
|
261
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, video);
|
|
262
|
+
}
|
|
263
|
+
gl.uniform1i(uniform('uVideo'), 0);
|
|
264
|
+
gl.uniform2f(uniform('uVideoSize'), video.videoWidth, video.videoHeight);
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<div
|
|
270
|
+
className={cn('elyri-pixel-vortex', className)}
|
|
271
|
+
data-scheme={scheme}
|
|
272
|
+
style={{ '--elyri-pixel-vortex-color': color } as CSSProperties}
|
|
273
|
+
>
|
|
274
|
+
<canvas ref={canvasRef} className="elyri-pixel-vortex__canvas" aria-hidden="true" />
|
|
275
|
+
{children != null && <div className="elyri-pixel-vortex__content">{children}</div>}
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|