@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 轻爪科技
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,71 @@
1
- # Temporary Holding Version
1
+ # @elyri/motion
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React 动效组件库,零运行时依赖,使用 CSS 变量做主题定制。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @elyri/motion
9
+ ```
10
+
11
+ ## 使用
12
+
13
+ ```tsx
14
+ import { FadeIn, GradientText } from '@elyri/motion';
15
+ import '@elyri/motion/styles.css';
16
+
17
+ export function Hero() {
18
+ return (
19
+ <FadeIn direction="up" delay={100}>
20
+ <GradientText colors={['#6d5cff', '#22d3ee']}>Elyri</GradientText>
21
+ </FadeIn>
22
+ );
23
+ }
24
+ ```
25
+
26
+ 也可以用 CLI 把组件源码拷进项目自行修改:
27
+
28
+ ```bash
29
+ pnpm dlx @elyri/cli@latest add fade-in gradient-text
30
+ ```
31
+
32
+ ## 主题
33
+
34
+ 所有可定制项都是 CSS 变量,定义在 `:root` 上即可全局覆盖,也可以只写在某个容器内做局部覆盖。
35
+
36
+ ```css
37
+ :root {
38
+ --elyri-duration-base: 500ms;
39
+ --elyri-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
40
+ }
41
+ ```
42
+
43
+ ## 组件
44
+
45
+ | 组件 | 分类 | 说明 |
46
+ | -------------- | ----------- | ----------------------------------------------- |
47
+ | `GradientText` | text | 渐变文字,支持自定义配色与流动动画 |
48
+ | `FadeIn` | animations | 进入视口时淡入位移 |
49
+ | `DragonScales` | backgrounds | WebGL 龙鳞动态背景,光源跟随指针 |
50
+ | `Aurora` | backgrounds | WebGL 极光夜空,多层光帘与星空 |
51
+ | `SilkWaves` | backgrounds | WebGL 流动丝绸,缎面光泽 |
52
+ | `Caustics` | backgrounds | WebGL 水下焦散光网,指针荡开水波 |
53
+ | `Velvet` | backgrounds | WebGL 垂坠丝绒,褶皱泛起绒光,指针划出渐消压痕 |
54
+ | `Planet` | backgrounds | WebGL 轨道视角星球(火星/木星),大气辉光与日出 |
55
+ | `MeshGradient` | backgrounds | WebGL 流光渐变,彩色丝带缓缓流淌并带缎面光泽 |
56
+ | `Halo` | backgrounds | WebGL 极简细光环,高光沿圆周缓缓流动 |
57
+ | `DotField` | backgrounds | WebGL 呼吸点阵,指针处形成放大透镜 |
58
+ | `Contours` | backgrounds | WebGL 缓慢漂移的等高线,指针处隆起小山 |
59
+ | `Filaments` | backgrounds | WebGL 交织光丝,如被轻拨的琴弦般起伏 |
60
+ | `FlipTiles` | backgrounds | WebGL 倒角立方体阵列,成波滚动翻出强调色侧面 |
61
+
62
+ ## Hooks
63
+
64
+ | Hook / Provider | 说明 |
65
+ | ------------------------- | ------------------------------------------- |
66
+ | `usePrefersReducedMotion` | 读取系统「减弱动态效果」偏好 |
67
+ | `ReducedMotionProvider` | 强制某棵子树播放 / 静止,优先级高于系统偏好 |
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,230 @@
1
+ import { createContext, useSyncExternalStore, useContext, useRef, useEffect } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/hooks/useColorScheme.ts
5
+ var readColorScheme = () => {
6
+ const root = document.documentElement;
7
+ const attr = root.dataset.theme;
8
+ if (attr === "light" || attr === "dark") return attr;
9
+ if (root.classList.contains("light")) return "light";
10
+ if (root.classList.contains("dark")) return "dark";
11
+ return "dark";
12
+ };
13
+ var subscribe = (onChange) => {
14
+ const observer = new MutationObserver(onChange);
15
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class"] });
16
+ return () => observer.disconnect();
17
+ };
18
+ var getServerSnapshot = () => "dark";
19
+ function useColorScheme() {
20
+ return useSyncExternalStore(subscribe, readColorScheme, getServerSnapshot);
21
+ }
22
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
23
+ var mediaQuery = null;
24
+ var query = () => mediaQuery ??= window.matchMedia(REDUCED_MOTION_QUERY);
25
+ var subscribe2 = (onChange) => {
26
+ const media = query();
27
+ media.addEventListener("change", onChange);
28
+ return () => media.removeEventListener("change", onChange);
29
+ };
30
+ var getSnapshot = () => query().matches;
31
+ var getServerSnapshot2 = () => false;
32
+ var ReducedMotionContext = createContext(null);
33
+ function ReducedMotionProvider({ reduced, children }) {
34
+ return /* @__PURE__ */ jsx(ReducedMotionContext.Provider, { value: reduced, children });
35
+ }
36
+ function usePrefersReducedMotion() {
37
+ const override = useContext(ReducedMotionContext);
38
+ const system = useSyncExternalStore(subscribe2, getSnapshot, getServerSnapshot2);
39
+ return override ?? system;
40
+ }
41
+ var VERTEX_SHADER = `
42
+ attribute vec2 aPosition;
43
+ void main() {
44
+ gl_Position = vec4(aPosition, 0.0, 1.0);
45
+ }
46
+ `;
47
+ var FRAME_INTERVAL = 1e3 / 60 - 1;
48
+ var compile = (gl, fragmentShader) => {
49
+ const program = gl.createProgram();
50
+ if (!program) return null;
51
+ for (const [type, source] of [
52
+ [gl.VERTEX_SHADER, VERTEX_SHADER],
53
+ [gl.FRAGMENT_SHADER, fragmentShader]
54
+ ]) {
55
+ const shader = gl.createShader(type);
56
+ if (!shader) return null;
57
+ gl.shaderSource(shader, source);
58
+ gl.compileShader(shader);
59
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
60
+ console.error("[elyri] shader compile failed:", gl.getShaderInfoLog(shader));
61
+ }
62
+ gl.attachShader(program, shader);
63
+ gl.deleteShader(shader);
64
+ }
65
+ gl.linkProgram(program);
66
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
67
+ console.error("[elyri] shader link failed:", gl.getProgramInfoLog(program));
68
+ gl.deleteProgram(program);
69
+ return null;
70
+ }
71
+ return program;
72
+ };
73
+ function useShaderCanvas(options) {
74
+ const canvasRef = useRef(null);
75
+ const optionsRef = useRef(options);
76
+ const redrawRef = useRef(void 0);
77
+ const prefersReducedMotion = usePrefersReducedMotion();
78
+ const { fragmentShader } = options;
79
+ useEffect(() => {
80
+ optionsRef.current = options;
81
+ redrawRef.current?.();
82
+ });
83
+ useEffect(() => {
84
+ const canvas = canvasRef.current;
85
+ const gl = canvas?.getContext("webgl", { alpha: false, antialias: false });
86
+ if (!canvas || !gl) return;
87
+ const program = compile(gl, fragmentShader);
88
+ if (!program) return;
89
+ const buffer = gl.createBuffer();
90
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
91
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
92
+ gl.useProgram(program);
93
+ const position = gl.getAttribLocation(program, "aPosition");
94
+ gl.enableVertexAttribArray(position);
95
+ gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
96
+ const locations = /* @__PURE__ */ new Map();
97
+ const uniform = (name) => {
98
+ if (!locations.has(name)) locations.set(name, gl.getUniformLocation(program, name));
99
+ return locations.get(name) ?? null;
100
+ };
101
+ let width = 1;
102
+ let height = 1;
103
+ let dpr = 1;
104
+ let time = prefersReducedMotion ? optionsRef.current.stillTime ?? 2.4 : 0;
105
+ const light = { x: 0.62, y: 0.32 };
106
+ let pointer = null;
107
+ const wander = () => ({ x: 0.5 + 0.32 * Math.sin(time * 0.31), y: 0.42 + 0.26 * Math.cos(time * 0.23) });
108
+ const draw = () => {
109
+ gl.uniform2f(uniform("uResolution"), width, height);
110
+ gl.uniform1f(uniform("uTime"), time);
111
+ gl.uniform2f(uniform("uPointer"), light.x * width, light.y * height);
112
+ optionsRef.current.onDraw?.({ gl, uniform, dpr });
113
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
114
+ canvas.dataset.ready = "";
115
+ };
116
+ const resize = () => {
117
+ const rect = canvas.getBoundingClientRect();
118
+ dpr = Math.min(window.devicePixelRatio || 1, optionsRef.current.maxDpr ?? 2);
119
+ width = Math.max(1, Math.round(rect.width * dpr));
120
+ height = Math.max(1, Math.round(rect.height * dpr));
121
+ canvas.width = width;
122
+ canvas.height = height;
123
+ gl.viewport(0, 0, width, height);
124
+ draw();
125
+ };
126
+ const handlePointerMove = (event) => {
127
+ const rect = canvas.getBoundingClientRect();
128
+ const x = (event.clientX - rect.left) / rect.width;
129
+ const y = (event.clientY - rect.top) / rect.height;
130
+ pointer = x >= 0 && x <= 1 && y >= 0 && y <= 1 ? { x, y } : null;
131
+ if (prefersReducedMotion && pointer && (optionsRef.current.interactive ?? true)) {
132
+ Object.assign(light, pointer);
133
+ draw();
134
+ }
135
+ };
136
+ const handlePointerLeave = () => {
137
+ pointer = null;
138
+ };
139
+ let frame = 0;
140
+ let last = 0;
141
+ let inView = true;
142
+ const tick = (now) => {
143
+ frame = requestAnimationFrame(tick);
144
+ if (last && now - last < FRAME_INTERVAL) return;
145
+ const dt = last ? Math.min((now - last) / 1e3, 0.1) : 0;
146
+ last = now;
147
+ const { speed = 1, interactive = true } = optionsRef.current;
148
+ time += dt * speed;
149
+ const target = pointer && interactive ? pointer : wander();
150
+ const ease = 1 - Math.exp(-dt * 4);
151
+ light.x += (target.x - light.x) * ease;
152
+ light.y += (target.y - light.y) * ease;
153
+ draw();
154
+ };
155
+ const start = () => {
156
+ if (prefersReducedMotion || frame || !inView || document.hidden) return;
157
+ last = 0;
158
+ frame = requestAnimationFrame(tick);
159
+ };
160
+ const stop = () => {
161
+ cancelAnimationFrame(frame);
162
+ frame = 0;
163
+ };
164
+ const handleVisibility = () => document.hidden ? stop() : start();
165
+ const resizeObserver = new ResizeObserver(resize);
166
+ resizeObserver.observe(canvas);
167
+ const intersectionObserver = new IntersectionObserver(([entry]) => {
168
+ inView = entry.isIntersecting;
169
+ if (inView) start();
170
+ else stop();
171
+ });
172
+ intersectionObserver.observe(canvas);
173
+ window.addEventListener("pointermove", handlePointerMove, { passive: true });
174
+ document.documentElement.addEventListener("pointerleave", handlePointerLeave);
175
+ document.addEventListener("visibilitychange", handleVisibility);
176
+ redrawRef.current = () => {
177
+ if (prefersReducedMotion) draw();
178
+ };
179
+ resize();
180
+ start();
181
+ return () => {
182
+ stop();
183
+ redrawRef.current = void 0;
184
+ resizeObserver.disconnect();
185
+ intersectionObserver.disconnect();
186
+ window.removeEventListener("pointermove", handlePointerMove);
187
+ document.documentElement.removeEventListener("pointerleave", handlePointerLeave);
188
+ document.removeEventListener("visibilitychange", handleVisibility);
189
+ gl.deleteBuffer(buffer);
190
+ gl.deleteProgram(program);
191
+ };
192
+ }, [fragmentShader, prefersReducedMotion]);
193
+ return canvasRef;
194
+ }
195
+
196
+ // src/utils/cn.ts
197
+ function cn(...values) {
198
+ const classes = [];
199
+ for (const value of values) {
200
+ if (!value) continue;
201
+ classes.push(Array.isArray(value) ? cn(...value) : String(value));
202
+ }
203
+ return classes.filter(Boolean).join(" ");
204
+ }
205
+
206
+ // src/utils/color.ts
207
+ function hexToLinearRgb(hex) {
208
+ let value = hex.replace("#", "");
209
+ if (value.length === 3) value = [...value].map((char) => char + char).join("");
210
+ const int = parseInt(value.slice(0, 6), 16);
211
+ return [int >> 16 & 255, int >> 8 & 255, int & 255].map((channel) => (channel / 255) ** 2.2);
212
+ }
213
+
214
+ // src/utils/text.ts
215
+ var splitTextUnits = (text, by) => {
216
+ let index = 0;
217
+ return text.split(/(\s+)/).filter(Boolean).map((part, partIndex) => {
218
+ if (/^\s+$/.test(part)) return { kind: "space", key: `space-${partIndex}`, value: part };
219
+ const units = by === "char" ? Array.from(part) : [part];
220
+ return {
221
+ kind: "word",
222
+ key: `word-${partIndex}`,
223
+ units: units.map((char) => ({ char, index: index++ }))
224
+ };
225
+ });
226
+ };
227
+
228
+ export { ReducedMotionProvider, cn, hexToLinearRgb, splitTextUnits, useColorScheme, usePrefersReducedMotion, useShaderCanvas };
229
+ //# sourceMappingURL=chunk-A7JCTGZO.js.map
230
+ //# sourceMappingURL=chunk-A7JCTGZO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/useColorScheme.ts","../src/hooks/usePrefersReducedMotion.tsx","../src/hooks/useShaderCanvas.ts","../src/utils/cn.ts","../src/utils/color.ts","../src/utils/text.ts"],"names":["subscribe","getServerSnapshot","useSyncExternalStore"],"mappings":"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","file":"chunk-A7JCTGZO.js","sourcesContent":["import { useSyncExternalStore } from 'react';\n\nexport type ColorScheme = 'light' | 'dark';\n\n/** 从页面根元素读取当前主题:优先 data-theme,其次 .dark / .light 类名,默认深色 */\nconst readColorScheme = (): ColorScheme => {\n const root = document.documentElement;\n const attr = root.dataset.theme;\n if (attr === 'light' || attr === 'dark') return attr;\n if (root.classList.contains('light')) return 'light';\n if (root.classList.contains('dark')) return 'dark';\n return 'dark';\n};\n\nconst subscribe = (onChange: () => void) => {\n const observer = new MutationObserver(onChange);\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });\n return () => observer.disconnect();\n};\n\n/** 服务端渲染时按深色输出,水合后再切到页面的真实主题 */\nconst getServerSnapshot = (): ColorScheme => 'dark';\n\n/**\n * 跟随页面主题实时切换明暗:主题由页面(<html data-theme> 或 .dark/.light)决定,\n * 组件只做镜像,不读取系统偏好。\n */\nexport function useColorScheme(): ColorScheme {\n return useSyncExternalStore(subscribe, readColorScheme, getServerSnapshot);\n}\n","import { createContext, useContext, useSyncExternalStore } from 'react';\nimport type { ReactNode } from 'react';\n\nconst REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';\n\nlet mediaQuery: MediaQueryList | null = null;\nconst query = () => (mediaQuery ??= window.matchMedia(REDUCED_MOTION_QUERY));\n\nconst subscribe = (onChange: () => void) => {\n const media = query();\n media.addEventListener('change', onChange);\n return () => media.removeEventListener('change', onChange);\n};\n\nconst getSnapshot = () => query().matches;\n\n/** 服务端渲染时先按 false 输出,水合后再切到真实值 */\nconst getServerSnapshot = () => false;\n\n/** 上层可强制一棵子树进入 / 退出「减弱动态效果」,null 表示跟随系统 */\nconst ReducedMotionContext = createContext<boolean | null>(null);\n\nexport interface ReducedMotionProviderProps {\n /** true 时子树内的动画只呈现静止状态,false 强制播放 */\n reduced: boolean;\n children: ReactNode;\n}\n\n/**\n * 在不改动系统设置的前提下,强制某棵子树播放或静止。\n * 子树内的动画都读取该值,优先级高于系统偏好。\n */\nexport function ReducedMotionProvider({ reduced, children }: ReducedMotionProviderProps) {\n return <ReducedMotionContext.Provider value={reduced}>{children}</ReducedMotionContext.Provider>;\n}\n\nexport function usePrefersReducedMotion(): boolean {\n const override = useContext(ReducedMotionContext);\n const system = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n return override ?? system;\n}\n","import { useEffect, useRef } from 'react';\n\nimport { usePrefersReducedMotion } from './usePrefersReducedMotion';\n\nconst VERTEX_SHADER = `\nattribute vec2 aPosition;\nvoid main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n}\n`;\n\n/** 绘制帧率上限;留 1ms 容差,避免 60Hz 屏上因 rAF 抖动被误判而丢帧 */\nconst FRAME_INTERVAL = 1000 / 60 - 1;\n\nexport interface ShaderFrame {\n gl: WebGLRenderingContext;\n /** 按名称取 uniform 位置(带缓存) */\n uniform: (name: string) => WebGLUniformLocation | null;\n /** 当前使用的设备像素比 */\n dpr: number;\n}\n\nexport interface ShaderCanvasOptions {\n /**\n * 全屏片元着色器。每帧自动写入:\n * - uResolution:画布尺寸(设备像素)\n * - uTime:累计动画时间(秒,已乘 speed)\n * - uPointer:平滑后的指针位置(设备像素,原点左上);无指针时沿轨迹缓慢游走\n */\n fragmentShader: string;\n /** 每次绘制前写入组件自定义的 uniform */\n onDraw?: (frame: ShaderFrame) => void;\n speed?: number;\n interactive?: boolean;\n /** 减弱动态效果时停留的时间点 */\n stillTime?: number;\n /** 内部渲染分辨率上限(设备像素比),默认 2;重着色器可调低以省填充率 */\n maxDpr?: number;\n}\n\nconst compile = (gl: WebGLRenderingContext, fragmentShader: string) => {\n const program = gl.createProgram();\n if (!program) return null;\n\n for (const [type, source] of [\n [gl.VERTEX_SHADER, VERTEX_SHADER],\n [gl.FRAGMENT_SHADER, fragmentShader],\n ] as const) {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('[elyri] shader compile failed:', gl.getShaderInfoLog(shader));\n }\n gl.attachShader(program, shader);\n gl.deleteShader(shader);\n }\n\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('[elyri] shader link failed:', gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n return null;\n }\n return program;\n};\n\n/**\n * 把一个全屏片元着色器渲染到 canvas 上:负责尺寸与 DPR、指针跟随、\n * 离屏/后台暂停,以及减弱动态效果时只绘制静止帧。\n */\nexport function useShaderCanvas(options: ShaderCanvasOptions) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const optionsRef = useRef(options);\n const redrawRef = useRef<() => void>(undefined);\n const prefersReducedMotion = usePrefersReducedMotion();\n const { fragmentShader } = options;\n\n // 每次渲染同步最新参数;静止模式下参数变化需要手动重绘\n useEffect(() => {\n optionsRef.current = options;\n redrawRef.current?.();\n });\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const gl = canvas?.getContext('webgl', { alpha: false, antialias: false });\n if (!canvas || !gl) return;\n\n const program = compile(gl, fragmentShader);\n if (!program) return;\n\n // 单个覆盖全屏的三角形\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n gl.useProgram(program);\n const position = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(position);\n gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n const locations = new Map<string, WebGLUniformLocation | null>();\n const uniform = (name: string) => {\n if (!locations.has(name)) locations.set(name, gl.getUniformLocation(program, name));\n return locations.get(name) ?? null;\n };\n\n let width = 1;\n let height = 1;\n let dpr = 1;\n let time = prefersReducedMotion ? (optionsRef.current.stillTime ?? 2.4) : 0;\n // 指针位置用 0-1 的相对坐标\n const light = { x: 0.62, y: 0.32 };\n let pointer: { x: number; y: number } | null = null;\n\n const wander = () => ({ x: 0.5 + 0.32 * Math.sin(time * 0.31), y: 0.42 + 0.26 * Math.cos(time * 0.23) });\n\n const draw = () => {\n gl.uniform2f(uniform('uResolution'), width, height);\n gl.uniform1f(uniform('uTime'), time);\n gl.uniform2f(uniform('uPointer'), light.x * width, light.y * height);\n optionsRef.current.onDraw?.({ gl, uniform, dpr });\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n canvas.dataset.ready = '';\n };\n\n const resize = () => {\n const rect = canvas.getBoundingClientRect();\n dpr = Math.min(window.devicePixelRatio || 1, optionsRef.current.maxDpr ?? 2);\n width = Math.max(1, Math.round(rect.width * dpr));\n height = Math.max(1, Math.round(rect.height * dpr));\n canvas.width = width;\n canvas.height = height;\n gl.viewport(0, 0, width, height);\n draw();\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = canvas.getBoundingClientRect();\n const x = (event.clientX - rect.left) / rect.width;\n const y = (event.clientY - rect.top) / rect.height;\n pointer = x >= 0 && x <= 1 && y >= 0 && y <= 1 ? { x, y } : null;\n if (prefersReducedMotion && pointer && (optionsRef.current.interactive ?? true)) {\n Object.assign(light, pointer);\n draw();\n }\n };\n const handlePointerLeave = () => {\n pointer = null;\n };\n\n let frame = 0;\n let last = 0;\n let inView = true;\n\n const tick = (now: number) => {\n frame = requestAnimationFrame(tick);\n if (last && now - last < FRAME_INTERVAL) return;\n const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;\n last = now;\n const { speed = 1, interactive = true } = optionsRef.current;\n time += dt * speed;\n const target = pointer && interactive ? pointer : wander();\n const ease = 1 - Math.exp(-dt * 4);\n light.x += (target.x - light.x) * ease;\n light.y += (target.y - light.y) * ease;\n draw();\n };\n\n const start = () => {\n if (prefersReducedMotion || frame || !inView || document.hidden) return;\n last = 0;\n frame = requestAnimationFrame(tick);\n };\n const stop = () => {\n cancelAnimationFrame(frame);\n frame = 0;\n };\n const handleVisibility = () => (document.hidden ? stop() : start());\n\n const resizeObserver = new ResizeObserver(resize);\n resizeObserver.observe(canvas);\n const intersectionObserver = new IntersectionObserver(([entry]) => {\n inView = entry.isIntersecting;\n if (inView) start();\n else stop();\n });\n intersectionObserver.observe(canvas);\n\n window.addEventListener('pointermove', handlePointerMove, { passive: true });\n document.documentElement.addEventListener('pointerleave', handlePointerLeave);\n document.addEventListener('visibilitychange', handleVisibility);\n redrawRef.current = () => {\n if (prefersReducedMotion) draw();\n };\n\n resize();\n start();\n\n return () => {\n stop();\n redrawRef.current = undefined;\n resizeObserver.disconnect();\n intersectionObserver.disconnect();\n window.removeEventListener('pointermove', handlePointerMove);\n document.documentElement.removeEventListener('pointerleave', handlePointerLeave);\n document.removeEventListener('visibilitychange', handleVisibility);\n gl.deleteBuffer(buffer);\n gl.deleteProgram(program);\n };\n }, [fragmentShader, prefersReducedMotion]);\n\n return canvasRef;\n}\n","export type ClassValue = string | number | null | undefined | false | ClassValue[];\n\nexport function cn(...values: ClassValue[]): string {\n const classes: string[] = [];\n\n for (const value of values) {\n if (!value) continue;\n classes.push(Array.isArray(value) ? cn(...value) : String(value));\n }\n\n return classes.filter(Boolean).join(' ');\n}\n","export type LinearRgb = [number, number, number];\n\n/** hex(#rgb / #rrggbb)转线性空间 RGB,供着色器在线性空间计算光照 */\nexport function hexToLinearRgb(hex: string): LinearRgb {\n let value = hex.replace('#', '');\n if (value.length === 3) value = [...value].map((char) => char + char).join('');\n const int = parseInt(value.slice(0, 6), 16);\n return [(int >> 16) & 255, (int >> 8) & 255, int & 255].map((channel) => (channel / 255) ** 2.2) as LinearRgb;\n}\n","export type SplitTextBy = 'char' | 'word';\n\nexport interface SplitTextUnit {\n char: string;\n index: number;\n}\n\nexport type SplitTextSegment =\n { kind: 'space'; key: string; value: string } | { kind: 'word'; key: string; units: SplitTextUnit[] };\n\n/** 按空白切分文本,保留空白片段,并为每个动画单元分配全局序号 */\nexport const splitTextUnits = (text: string, by: SplitTextBy): SplitTextSegment[] => {\n let index = 0;\n\n return text\n .split(/(\\s+)/)\n .filter(Boolean)\n .map((part, partIndex) => {\n if (/^\\s+$/.test(part)) return { kind: 'space' as const, key: `space-${partIndex}`, value: part };\n\n const units = by === 'char' ? Array.from(part) : [part];\n\n return {\n kind: 'word' as const,\n key: `word-${partIndex}`,\n units: units.map((char) => ({ char, index: index++ })),\n };\n });\n};\n"]}
package/dist/core.cjs ADDED
@@ -0,0 +1,238 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/hooks/useColorScheme.ts
7
+ var readColorScheme = () => {
8
+ const root = document.documentElement;
9
+ const attr = root.dataset.theme;
10
+ if (attr === "light" || attr === "dark") return attr;
11
+ if (root.classList.contains("light")) return "light";
12
+ if (root.classList.contains("dark")) return "dark";
13
+ return "dark";
14
+ };
15
+ var subscribe = (onChange) => {
16
+ const observer = new MutationObserver(onChange);
17
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class"] });
18
+ return () => observer.disconnect();
19
+ };
20
+ var getServerSnapshot = () => "dark";
21
+ function useColorScheme() {
22
+ return react.useSyncExternalStore(subscribe, readColorScheme, getServerSnapshot);
23
+ }
24
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
25
+ var mediaQuery = null;
26
+ var query = () => mediaQuery ??= window.matchMedia(REDUCED_MOTION_QUERY);
27
+ var subscribe2 = (onChange) => {
28
+ const media = query();
29
+ media.addEventListener("change", onChange);
30
+ return () => media.removeEventListener("change", onChange);
31
+ };
32
+ var getSnapshot = () => query().matches;
33
+ var getServerSnapshot2 = () => false;
34
+ var ReducedMotionContext = react.createContext(null);
35
+ function ReducedMotionProvider({ reduced, children }) {
36
+ return /* @__PURE__ */ jsxRuntime.jsx(ReducedMotionContext.Provider, { value: reduced, children });
37
+ }
38
+ function usePrefersReducedMotion() {
39
+ const override = react.useContext(ReducedMotionContext);
40
+ const system = react.useSyncExternalStore(subscribe2, getSnapshot, getServerSnapshot2);
41
+ return override ?? system;
42
+ }
43
+ var VERTEX_SHADER = `
44
+ attribute vec2 aPosition;
45
+ void main() {
46
+ gl_Position = vec4(aPosition, 0.0, 1.0);
47
+ }
48
+ `;
49
+ var FRAME_INTERVAL = 1e3 / 60 - 1;
50
+ var compile = (gl, fragmentShader) => {
51
+ const program = gl.createProgram();
52
+ if (!program) return null;
53
+ for (const [type, source] of [
54
+ [gl.VERTEX_SHADER, VERTEX_SHADER],
55
+ [gl.FRAGMENT_SHADER, fragmentShader]
56
+ ]) {
57
+ const shader = gl.createShader(type);
58
+ if (!shader) return null;
59
+ gl.shaderSource(shader, source);
60
+ gl.compileShader(shader);
61
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
62
+ console.error("[elyri] shader compile failed:", gl.getShaderInfoLog(shader));
63
+ }
64
+ gl.attachShader(program, shader);
65
+ gl.deleteShader(shader);
66
+ }
67
+ gl.linkProgram(program);
68
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
69
+ console.error("[elyri] shader link failed:", gl.getProgramInfoLog(program));
70
+ gl.deleteProgram(program);
71
+ return null;
72
+ }
73
+ return program;
74
+ };
75
+ function useShaderCanvas(options) {
76
+ const canvasRef = react.useRef(null);
77
+ const optionsRef = react.useRef(options);
78
+ const redrawRef = react.useRef(void 0);
79
+ const prefersReducedMotion = usePrefersReducedMotion();
80
+ const { fragmentShader } = options;
81
+ react.useEffect(() => {
82
+ optionsRef.current = options;
83
+ redrawRef.current?.();
84
+ });
85
+ react.useEffect(() => {
86
+ const canvas = canvasRef.current;
87
+ const gl = canvas?.getContext("webgl", { alpha: false, antialias: false });
88
+ if (!canvas || !gl) return;
89
+ const program = compile(gl, fragmentShader);
90
+ if (!program) return;
91
+ const buffer = gl.createBuffer();
92
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
93
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
94
+ gl.useProgram(program);
95
+ const position = gl.getAttribLocation(program, "aPosition");
96
+ gl.enableVertexAttribArray(position);
97
+ gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
98
+ const locations = /* @__PURE__ */ new Map();
99
+ const uniform = (name) => {
100
+ if (!locations.has(name)) locations.set(name, gl.getUniformLocation(program, name));
101
+ return locations.get(name) ?? null;
102
+ };
103
+ let width = 1;
104
+ let height = 1;
105
+ let dpr = 1;
106
+ let time = prefersReducedMotion ? optionsRef.current.stillTime ?? 2.4 : 0;
107
+ const light = { x: 0.62, y: 0.32 };
108
+ let pointer = null;
109
+ const wander = () => ({ x: 0.5 + 0.32 * Math.sin(time * 0.31), y: 0.42 + 0.26 * Math.cos(time * 0.23) });
110
+ const draw = () => {
111
+ gl.uniform2f(uniform("uResolution"), width, height);
112
+ gl.uniform1f(uniform("uTime"), time);
113
+ gl.uniform2f(uniform("uPointer"), light.x * width, light.y * height);
114
+ optionsRef.current.onDraw?.({ gl, uniform, dpr });
115
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
116
+ canvas.dataset.ready = "";
117
+ };
118
+ const resize = () => {
119
+ const rect = canvas.getBoundingClientRect();
120
+ dpr = Math.min(window.devicePixelRatio || 1, optionsRef.current.maxDpr ?? 2);
121
+ width = Math.max(1, Math.round(rect.width * dpr));
122
+ height = Math.max(1, Math.round(rect.height * dpr));
123
+ canvas.width = width;
124
+ canvas.height = height;
125
+ gl.viewport(0, 0, width, height);
126
+ draw();
127
+ };
128
+ const handlePointerMove = (event) => {
129
+ const rect = canvas.getBoundingClientRect();
130
+ const x = (event.clientX - rect.left) / rect.width;
131
+ const y = (event.clientY - rect.top) / rect.height;
132
+ pointer = x >= 0 && x <= 1 && y >= 0 && y <= 1 ? { x, y } : null;
133
+ if (prefersReducedMotion && pointer && (optionsRef.current.interactive ?? true)) {
134
+ Object.assign(light, pointer);
135
+ draw();
136
+ }
137
+ };
138
+ const handlePointerLeave = () => {
139
+ pointer = null;
140
+ };
141
+ let frame = 0;
142
+ let last = 0;
143
+ let inView = true;
144
+ const tick = (now) => {
145
+ frame = requestAnimationFrame(tick);
146
+ if (last && now - last < FRAME_INTERVAL) return;
147
+ const dt = last ? Math.min((now - last) / 1e3, 0.1) : 0;
148
+ last = now;
149
+ const { speed = 1, interactive = true } = optionsRef.current;
150
+ time += dt * speed;
151
+ const target = pointer && interactive ? pointer : wander();
152
+ const ease = 1 - Math.exp(-dt * 4);
153
+ light.x += (target.x - light.x) * ease;
154
+ light.y += (target.y - light.y) * ease;
155
+ draw();
156
+ };
157
+ const start = () => {
158
+ if (prefersReducedMotion || frame || !inView || document.hidden) return;
159
+ last = 0;
160
+ frame = requestAnimationFrame(tick);
161
+ };
162
+ const stop = () => {
163
+ cancelAnimationFrame(frame);
164
+ frame = 0;
165
+ };
166
+ const handleVisibility = () => document.hidden ? stop() : start();
167
+ const resizeObserver = new ResizeObserver(resize);
168
+ resizeObserver.observe(canvas);
169
+ const intersectionObserver = new IntersectionObserver(([entry]) => {
170
+ inView = entry.isIntersecting;
171
+ if (inView) start();
172
+ else stop();
173
+ });
174
+ intersectionObserver.observe(canvas);
175
+ window.addEventListener("pointermove", handlePointerMove, { passive: true });
176
+ document.documentElement.addEventListener("pointerleave", handlePointerLeave);
177
+ document.addEventListener("visibilitychange", handleVisibility);
178
+ redrawRef.current = () => {
179
+ if (prefersReducedMotion) draw();
180
+ };
181
+ resize();
182
+ start();
183
+ return () => {
184
+ stop();
185
+ redrawRef.current = void 0;
186
+ resizeObserver.disconnect();
187
+ intersectionObserver.disconnect();
188
+ window.removeEventListener("pointermove", handlePointerMove);
189
+ document.documentElement.removeEventListener("pointerleave", handlePointerLeave);
190
+ document.removeEventListener("visibilitychange", handleVisibility);
191
+ gl.deleteBuffer(buffer);
192
+ gl.deleteProgram(program);
193
+ };
194
+ }, [fragmentShader, prefersReducedMotion]);
195
+ return canvasRef;
196
+ }
197
+
198
+ // src/utils/cn.ts
199
+ function cn(...values) {
200
+ const classes = [];
201
+ for (const value of values) {
202
+ if (!value) continue;
203
+ classes.push(Array.isArray(value) ? cn(...value) : String(value));
204
+ }
205
+ return classes.filter(Boolean).join(" ");
206
+ }
207
+
208
+ // src/utils/color.ts
209
+ function hexToLinearRgb(hex) {
210
+ let value = hex.replace("#", "");
211
+ if (value.length === 3) value = [...value].map((char) => char + char).join("");
212
+ const int = parseInt(value.slice(0, 6), 16);
213
+ return [int >> 16 & 255, int >> 8 & 255, int & 255].map((channel) => (channel / 255) ** 2.2);
214
+ }
215
+
216
+ // src/utils/text.ts
217
+ var splitTextUnits = (text, by) => {
218
+ let index = 0;
219
+ return text.split(/(\s+)/).filter(Boolean).map((part, partIndex) => {
220
+ if (/^\s+$/.test(part)) return { kind: "space", key: `space-${partIndex}`, value: part };
221
+ const units = by === "char" ? Array.from(part) : [part];
222
+ return {
223
+ kind: "word",
224
+ key: `word-${partIndex}`,
225
+ units: units.map((char) => ({ char, index: index++ }))
226
+ };
227
+ });
228
+ };
229
+
230
+ exports.ReducedMotionProvider = ReducedMotionProvider;
231
+ exports.cn = cn;
232
+ exports.hexToLinearRgb = hexToLinearRgb;
233
+ exports.splitTextUnits = splitTextUnits;
234
+ exports.useColorScheme = useColorScheme;
235
+ exports.usePrefersReducedMotion = usePrefersReducedMotion;
236
+ exports.useShaderCanvas = useShaderCanvas;
237
+ //# sourceMappingURL=core.cjs.map
238
+ //# sourceMappingURL=core.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/useColorScheme.ts","../src/hooks/usePrefersReducedMotion.tsx","../src/hooks/useShaderCanvas.ts","../src/utils/cn.ts","../src/utils/color.ts","../src/utils/text.ts"],"names":["useSyncExternalStore","subscribe","getServerSnapshot","createContext","useContext","useRef","useEffect"],"mappings":"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","file":"core.cjs","sourcesContent":["import { useSyncExternalStore } from 'react';\n\nexport type ColorScheme = 'light' | 'dark';\n\n/** 从页面根元素读取当前主题:优先 data-theme,其次 .dark / .light 类名,默认深色 */\nconst readColorScheme = (): ColorScheme => {\n const root = document.documentElement;\n const attr = root.dataset.theme;\n if (attr === 'light' || attr === 'dark') return attr;\n if (root.classList.contains('light')) return 'light';\n if (root.classList.contains('dark')) return 'dark';\n return 'dark';\n};\n\nconst subscribe = (onChange: () => void) => {\n const observer = new MutationObserver(onChange);\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });\n return () => observer.disconnect();\n};\n\n/** 服务端渲染时按深色输出,水合后再切到页面的真实主题 */\nconst getServerSnapshot = (): ColorScheme => 'dark';\n\n/**\n * 跟随页面主题实时切换明暗:主题由页面(<html data-theme> 或 .dark/.light)决定,\n * 组件只做镜像,不读取系统偏好。\n */\nexport function useColorScheme(): ColorScheme {\n return useSyncExternalStore(subscribe, readColorScheme, getServerSnapshot);\n}\n","import { createContext, useContext, useSyncExternalStore } from 'react';\nimport type { ReactNode } from 'react';\n\nconst REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';\n\nlet mediaQuery: MediaQueryList | null = null;\nconst query = () => (mediaQuery ??= window.matchMedia(REDUCED_MOTION_QUERY));\n\nconst subscribe = (onChange: () => void) => {\n const media = query();\n media.addEventListener('change', onChange);\n return () => media.removeEventListener('change', onChange);\n};\n\nconst getSnapshot = () => query().matches;\n\n/** 服务端渲染时先按 false 输出,水合后再切到真实值 */\nconst getServerSnapshot = () => false;\n\n/** 上层可强制一棵子树进入 / 退出「减弱动态效果」,null 表示跟随系统 */\nconst ReducedMotionContext = createContext<boolean | null>(null);\n\nexport interface ReducedMotionProviderProps {\n /** true 时子树内的动画只呈现静止状态,false 强制播放 */\n reduced: boolean;\n children: ReactNode;\n}\n\n/**\n * 在不改动系统设置的前提下,强制某棵子树播放或静止。\n * 子树内的动画都读取该值,优先级高于系统偏好。\n */\nexport function ReducedMotionProvider({ reduced, children }: ReducedMotionProviderProps) {\n return <ReducedMotionContext.Provider value={reduced}>{children}</ReducedMotionContext.Provider>;\n}\n\nexport function usePrefersReducedMotion(): boolean {\n const override = useContext(ReducedMotionContext);\n const system = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n return override ?? system;\n}\n","import { useEffect, useRef } from 'react';\n\nimport { usePrefersReducedMotion } from './usePrefersReducedMotion';\n\nconst VERTEX_SHADER = `\nattribute vec2 aPosition;\nvoid main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n}\n`;\n\n/** 绘制帧率上限;留 1ms 容差,避免 60Hz 屏上因 rAF 抖动被误判而丢帧 */\nconst FRAME_INTERVAL = 1000 / 60 - 1;\n\nexport interface ShaderFrame {\n gl: WebGLRenderingContext;\n /** 按名称取 uniform 位置(带缓存) */\n uniform: (name: string) => WebGLUniformLocation | null;\n /** 当前使用的设备像素比 */\n dpr: number;\n}\n\nexport interface ShaderCanvasOptions {\n /**\n * 全屏片元着色器。每帧自动写入:\n * - uResolution:画布尺寸(设备像素)\n * - uTime:累计动画时间(秒,已乘 speed)\n * - uPointer:平滑后的指针位置(设备像素,原点左上);无指针时沿轨迹缓慢游走\n */\n fragmentShader: string;\n /** 每次绘制前写入组件自定义的 uniform */\n onDraw?: (frame: ShaderFrame) => void;\n speed?: number;\n interactive?: boolean;\n /** 减弱动态效果时停留的时间点 */\n stillTime?: number;\n /** 内部渲染分辨率上限(设备像素比),默认 2;重着色器可调低以省填充率 */\n maxDpr?: number;\n}\n\nconst compile = (gl: WebGLRenderingContext, fragmentShader: string) => {\n const program = gl.createProgram();\n if (!program) return null;\n\n for (const [type, source] of [\n [gl.VERTEX_SHADER, VERTEX_SHADER],\n [gl.FRAGMENT_SHADER, fragmentShader],\n ] as const) {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error('[elyri] shader compile failed:', gl.getShaderInfoLog(shader));\n }\n gl.attachShader(program, shader);\n gl.deleteShader(shader);\n }\n\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error('[elyri] shader link failed:', gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n return null;\n }\n return program;\n};\n\n/**\n * 把一个全屏片元着色器渲染到 canvas 上:负责尺寸与 DPR、指针跟随、\n * 离屏/后台暂停,以及减弱动态效果时只绘制静止帧。\n */\nexport function useShaderCanvas(options: ShaderCanvasOptions) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const optionsRef = useRef(options);\n const redrawRef = useRef<() => void>(undefined);\n const prefersReducedMotion = usePrefersReducedMotion();\n const { fragmentShader } = options;\n\n // 每次渲染同步最新参数;静止模式下参数变化需要手动重绘\n useEffect(() => {\n optionsRef.current = options;\n redrawRef.current?.();\n });\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const gl = canvas?.getContext('webgl', { alpha: false, antialias: false });\n if (!canvas || !gl) return;\n\n const program = compile(gl, fragmentShader);\n if (!program) return;\n\n // 单个覆盖全屏的三角形\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n gl.useProgram(program);\n const position = gl.getAttribLocation(program, 'aPosition');\n gl.enableVertexAttribArray(position);\n gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n const locations = new Map<string, WebGLUniformLocation | null>();\n const uniform = (name: string) => {\n if (!locations.has(name)) locations.set(name, gl.getUniformLocation(program, name));\n return locations.get(name) ?? null;\n };\n\n let width = 1;\n let height = 1;\n let dpr = 1;\n let time = prefersReducedMotion ? (optionsRef.current.stillTime ?? 2.4) : 0;\n // 指针位置用 0-1 的相对坐标\n const light = { x: 0.62, y: 0.32 };\n let pointer: { x: number; y: number } | null = null;\n\n const wander = () => ({ x: 0.5 + 0.32 * Math.sin(time * 0.31), y: 0.42 + 0.26 * Math.cos(time * 0.23) });\n\n const draw = () => {\n gl.uniform2f(uniform('uResolution'), width, height);\n gl.uniform1f(uniform('uTime'), time);\n gl.uniform2f(uniform('uPointer'), light.x * width, light.y * height);\n optionsRef.current.onDraw?.({ gl, uniform, dpr });\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n canvas.dataset.ready = '';\n };\n\n const resize = () => {\n const rect = canvas.getBoundingClientRect();\n dpr = Math.min(window.devicePixelRatio || 1, optionsRef.current.maxDpr ?? 2);\n width = Math.max(1, Math.round(rect.width * dpr));\n height = Math.max(1, Math.round(rect.height * dpr));\n canvas.width = width;\n canvas.height = height;\n gl.viewport(0, 0, width, height);\n draw();\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const rect = canvas.getBoundingClientRect();\n const x = (event.clientX - rect.left) / rect.width;\n const y = (event.clientY - rect.top) / rect.height;\n pointer = x >= 0 && x <= 1 && y >= 0 && y <= 1 ? { x, y } : null;\n if (prefersReducedMotion && pointer && (optionsRef.current.interactive ?? true)) {\n Object.assign(light, pointer);\n draw();\n }\n };\n const handlePointerLeave = () => {\n pointer = null;\n };\n\n let frame = 0;\n let last = 0;\n let inView = true;\n\n const tick = (now: number) => {\n frame = requestAnimationFrame(tick);\n if (last && now - last < FRAME_INTERVAL) return;\n const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;\n last = now;\n const { speed = 1, interactive = true } = optionsRef.current;\n time += dt * speed;\n const target = pointer && interactive ? pointer : wander();\n const ease = 1 - Math.exp(-dt * 4);\n light.x += (target.x - light.x) * ease;\n light.y += (target.y - light.y) * ease;\n draw();\n };\n\n const start = () => {\n if (prefersReducedMotion || frame || !inView || document.hidden) return;\n last = 0;\n frame = requestAnimationFrame(tick);\n };\n const stop = () => {\n cancelAnimationFrame(frame);\n frame = 0;\n };\n const handleVisibility = () => (document.hidden ? stop() : start());\n\n const resizeObserver = new ResizeObserver(resize);\n resizeObserver.observe(canvas);\n const intersectionObserver = new IntersectionObserver(([entry]) => {\n inView = entry.isIntersecting;\n if (inView) start();\n else stop();\n });\n intersectionObserver.observe(canvas);\n\n window.addEventListener('pointermove', handlePointerMove, { passive: true });\n document.documentElement.addEventListener('pointerleave', handlePointerLeave);\n document.addEventListener('visibilitychange', handleVisibility);\n redrawRef.current = () => {\n if (prefersReducedMotion) draw();\n };\n\n resize();\n start();\n\n return () => {\n stop();\n redrawRef.current = undefined;\n resizeObserver.disconnect();\n intersectionObserver.disconnect();\n window.removeEventListener('pointermove', handlePointerMove);\n document.documentElement.removeEventListener('pointerleave', handlePointerLeave);\n document.removeEventListener('visibilitychange', handleVisibility);\n gl.deleteBuffer(buffer);\n gl.deleteProgram(program);\n };\n }, [fragmentShader, prefersReducedMotion]);\n\n return canvasRef;\n}\n","export type ClassValue = string | number | null | undefined | false | ClassValue[];\n\nexport function cn(...values: ClassValue[]): string {\n const classes: string[] = [];\n\n for (const value of values) {\n if (!value) continue;\n classes.push(Array.isArray(value) ? cn(...value) : String(value));\n }\n\n return classes.filter(Boolean).join(' ');\n}\n","export type LinearRgb = [number, number, number];\n\n/** hex(#rgb / #rrggbb)转线性空间 RGB,供着色器在线性空间计算光照 */\nexport function hexToLinearRgb(hex: string): LinearRgb {\n let value = hex.replace('#', '');\n if (value.length === 3) value = [...value].map((char) => char + char).join('');\n const int = parseInt(value.slice(0, 6), 16);\n return [(int >> 16) & 255, (int >> 8) & 255, int & 255].map((channel) => (channel / 255) ** 2.2) as LinearRgb;\n}\n","export type SplitTextBy = 'char' | 'word';\n\nexport interface SplitTextUnit {\n char: string;\n index: number;\n}\n\nexport type SplitTextSegment =\n { kind: 'space'; key: string; value: string } | { kind: 'word'; key: string; units: SplitTextUnit[] };\n\n/** 按空白切分文本,保留空白片段,并为每个动画单元分配全局序号 */\nexport const splitTextUnits = (text: string, by: SplitTextBy): SplitTextSegment[] => {\n let index = 0;\n\n return text\n .split(/(\\s+)/)\n .filter(Boolean)\n .map((part, partIndex) => {\n if (/^\\s+$/.test(part)) return { kind: 'space' as const, key: `space-${partIndex}`, value: part };\n\n const units = by === 'char' ? Array.from(part) : [part];\n\n return {\n kind: 'word' as const,\n key: `word-${partIndex}`,\n units: units.map((char) => ({ char, index: index++ })),\n };\n });\n};\n"]}