Enhance audio visualizers with new NebulaVisualizer and refactor existing components

- Introduce the NebulaVisualizer component, featuring particles that respond to audio input, enhancing the visual experience.
- Refactor AuraVisualizer, SpectrumVisualizer, and WaveVisualizer to utilize the adaptPalette function for improved theme handling.
- Update visualizer logic to enhance responsiveness and visual effects based on audio analysis, ensuring a cohesive user experience across components.
This commit is contained in:
Xin Wang
2026-06-10 09:17:14 +08:00
parent 6e83396d64
commit df7ce493f1
4 changed files with 313 additions and 69 deletions

View File

@@ -3,7 +3,12 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { useAudioAnalyser } from "@/hooks/use-audio-analyser";
import { readPalette, type RGB } from "@/lib/visualizer-palette";
import {
adaptPalette,
isDarkTheme,
readPalette,
type RGB,
} from "@/lib/visualizer-palette";
export type AuraVisualizerProps = {
/** 是否激活true 时采集麦克风并随音量律动false 时显示静态呼吸态 */
@@ -26,8 +31,9 @@ void main() {
}
`;
// 光环本体:极坐标下一个被 fbm 噪声轻微扰动边缘的柔光环 + 中心柔光
// 由 u_volume音量驱动缩放 / 亮度。配色随半径在三色间平滑过渡。
// 虹彩光环:一圈被 fbm 噪声轻微扰动、沿圆周流动三色虹彩的发光细环
// 环内漂浮两团缓慢游走的柔光“流体”。静态时缓慢呼吸,
// 激活后由 u_volume 驱动半径、亮度与扰动幅度。
const FRAG = `
precision highp float;
@@ -61,7 +67,7 @@ float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
for (int i = 0; i < 5; i++) {
for (int i = 0; i < 4; i++) {
v += a * noise(p);
p = m * p;
a *= 0.5;
@@ -69,6 +75,14 @@ float fbm(vec2 p) {
return v;
}
// 三色沿相位做环形虹彩插值cos 加权,首尾无缝)
vec3 iridescent(float a) {
float w0 = 0.5 + 0.5 * cos(a);
float w1 = 0.5 + 0.5 * cos(a - 2.0943951);
float w2 = 0.5 + 0.5 * cos(a - 4.1887902);
return (u_c0 * w0 + u_c1 * w1 + u_c2 * w2) / (w0 + w1 + w2);
}
// 静态抖动,消除平滑渐变上的色带
float dither(vec2 p) {
return (hash(p) - 0.5) / 255.0;
@@ -80,39 +94,57 @@ void main() {
float ang = atan(uv.y, uv.x);
float vol = clamp(u_volume, 0.0, 1.0) * u_active;
float breathe = 0.5 + 0.5 * sin(u_time * 0.8);
float breathe = 0.5 + 0.5 * sin(u_time * 0.7);
// 单层缓慢扰动的有机边缘
float n = fbm(vec2(cos(ang), sin(ang)) * 1.6 + u_time * 0.12);
float baseR = 0.32 + 0.03 * breathe + 0.09 * vol;
float edge = baseR + (n - 0.5) * (0.03 + 0.05 * vol);
float dr = r - edge;
// 基础半径:静态缓慢呼吸,说话时随音量扩张
float radius = 0.30 + 0.02 * breathe + 0.085 * vol;
float ring = exp(-dr * dr * 120.0); // 柔和发光环
float halo = exp(-r * r * 5.0); // 中心柔光
// 亮色模式削弱中心光晕,避免在白底上铺成灰雾
float intensity = ring * (0.9 + 0.6 * vol)
+ halo * (mix(0.28, 0.12, u_theme) + 0.4 * vol);
// 双层噪声扰动出有机的环形轮廓
vec2 dir = vec2(cos(ang), sin(ang));
float n1 = fbm(dir * 1.7 + u_time * 0.14);
float n2 = fbm(dir * 3.1 - u_time * 0.09 + 4.7);
float rr = radius
+ (n1 - 0.5) * (0.028 + 0.07 * vol)
+ (n2 - 0.5) * (0.012 + 0.035 * vol);
float d = r - rr;
// 平滑的径向配色,不随时间闪烁
vec3 col = mix(u_c0, u_c1, smoothstep(0.0, 0.55, r));
col = mix(col, u_c2, smoothstep(0.4, 0.95, r));
// 细亮的环芯 + 宽柔的辉光晕
float coreW = 0.008 + 0.005 * vol + 0.002 * breathe;
float core = exp(-d * d / (2.0 * coreW * coreW));
float bloomW = 0.055 + 0.055 * vol;
float bloom = exp(-d * d / (2.0 * bloomW * bloomW)) * 0.55;
// 亮色 token 偏浅:提升饱和并适度加深,使颜色在浅背景上不发灰
float luma = dot(col, vec3(0.299, 0.587, 0.114));
col = clamp(mix(vec3(luma), col, mix(1.2, 1.65, u_theme)), 0.0, 1.0);
col *= mix(1.0, 0.72, u_theme);
// 环内流体:两团缓慢游走的柔光
vec2 p1 = 0.13 * vec2(cos(u_time * 0.41), sin(u_time * 0.33));
vec2 p2 = 0.16 * vec2(cos(-u_time * 0.26 + 2.3), sin(u_time * 0.36 + 1.2));
vec2 q1 = uv - p1;
vec2 q2 = uv - p2;
float b1 = exp(-dot(q1, q1) / 0.022);
float b2 = exp(-dot(q2, q2) / 0.030);
float inside = smoothstep(rr + 0.01, rr - 0.07, r);
float neb = (0.8 * b1 + 0.65 * b2) * inside
* (0.22 + 0.10 * breathe + 0.55 * vol);
float bright = 0.85 + vol + 0.12 * breathe;
vec3 hdr = col * intensity * bright;
// 虹彩相位:沿圆周流动并被噪声轻微扭曲
float flow = ang + u_time * 0.25 + (n1 - 0.5) * 2.2;
vec3 ringCol = iridescent(flow);
vec3 nebCol = mix(
iridescent(u_time * 0.17 + 1.3),
iridescent(-u_time * 0.11 + 3.9),
0.5 + 0.5 * sin(u_time * 0.21)
);
// 暗色:辉光优雅泛白;亮色:保持饱和色,不向白过曝
vec3 darkMap = vec3(1.0) - exp(-hdr * 1.5);
vec3 lightMap = col * clamp(intensity * bright, 0.0, 1.0);
float ringI = core * (0.9 + 0.85 * vol + 0.1 * breathe)
+ bloom * (0.35 + 0.6 * vol + 0.08 * breathe);
vec3 hdr = ringCol * ringI + nebCol * neb;
// 暗色:高光优雅泛白;亮色:保持饱和色,不向白过曝
vec3 darkMap = vec3(1.0) - exp(-hdr * 1.55);
vec3 lightMap = clamp(ringCol * min(ringI, 1.0) + nebCol * neb, 0.0, 1.0);
vec3 mapped = mix(darkMap, lightMap, u_theme);
mapped += dither(gl_FragCoord.xy);
float alpha = clamp(intensity * mix(1.1, 1.4, u_theme), 0.0, 1.0);
float alpha = clamp((ringI + neb) * mix(1.15, 1.35, u_theme), 0.0, 1.0);
gl_FragColor = vec4(mapped, alpha);
}
`;
@@ -209,7 +241,7 @@ export function AuraVisualizer({
const draw = () => {
const t = (performance.now() - start) / 1000;
// 由频谱算出单一音量标量(低中频,人声主能量),平滑
// 由频谱算出单一音量标量(低中频,人声主能量),快攻慢放地平滑
const node = analyserRef.current;
let target = 0;
if (node) {
@@ -219,16 +251,15 @@ export function AuraVisualizer({
for (let i = 0; i < bins; i++) sum += freq[i];
target = sum / bins / 255;
}
volumeRef.current += (target - volumeRef.current) * 0.18;
const k = target > volumeRef.current ? 0.3 : 0.08;
volumeRef.current += (target - volumeRef.current) * k;
const { sky, lav, rose } = readPalette(canvas);
const light = document.documentElement.classList.contains("dark")
? 0
: 1;
const dark = isDarkTheme();
const { sky, lav, rose } = adaptPalette(readPalette(canvas), dark);
gl.uniform1f(uTime, t);
gl.uniform1f(uVol, volumeRef.current);
gl.uniform1f(uActive, activeRef.current ? 1 : 0);
gl.uniform1f(uTheme, light);
gl.uniform1f(uTheme, dark ? 0 : 1);
gl.uniform3fv(uC0, norm(sky));
gl.uniform3fv(uC1, norm(lav));
gl.uniform3fv(uC2, norm(rose));