@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
293 lines (281 loc) • 8.99 kB
JavaScript
"use client";
import { LANDING_PALETTE_DARK, LANDING_PALETTE_LIGHT } from "../landingTokens.mjs";
import { styles } from "./style.mjs";
import { memo, useEffect, useRef, useState } from "react";
import { jsx } from "react/jsx-runtime";
import { cx, useThemeMode } from "antd-style";
//#region src/awesome/FluidGradient/FluidGradient.tsx
const VERTEX_SHADER = `#version 300 es
in vec2 aPos;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
}`;
const FRAGMENT_SHADER = `#version 300 es
precision highp float;
uniform vec2 uRes;
uniform float uTime;
uniform vec3 uStopA;
uniform vec3 uStopB;
uniform vec3 uStopC;
uniform float uIntensity;
out vec4 fragColor;
float hash(vec2 p) {
p = fract(p * vec2(234.34, 435.345));
p += dot(p, p + 34.23);
return fract(p.x * p.y);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
for (int i = 0; i < 4; i++) {
value += amplitude * noise(p);
p = mat2(0.8, 0.6, -0.6, 0.8) * p * 2.0 + vec2(3.7, 1.3);
amplitude *= 0.5;
}
return value;
}
void main() {
vec2 uv = gl_FragCoord.xy / uRes;
vec2 p = vec2(uv.x * uRes.x / uRes.y, uv.y);
float t = uTime * 0.05;
vec2 q = vec2(fbm(p * 1.4 + t * 0.35), fbm(p * 1.4 - t * 0.28 + 5.2));
vec2 r = vec2(
fbm(p * 1.4 + 2.6 * q + vec2(1.7, 9.2) + t * 0.18),
fbm(p * 1.4 + 2.6 * q + vec2(8.3, 2.8) - t * 0.12)
);
float field = fbm(p * 1.4 + 2.6 * r);
vec3 color = mix(uStopA, uStopB, clamp(field * field * 2.4, 0.0, 1.0));
color = mix(color, uStopC, clamp(dot(q, vec2(0.7)) - 0.25, 0.0, 1.0));
float fade = smoothstep(0.02, 0.85, uv.y);
float alpha = fade * fade * uIntensity * smoothstep(0.14, 0.66, field);
fragColor = vec4(color * alpha, alpha);
}`;
const hexToRgb = (hex) => {
const value = hex.replace("#", "");
const full = value.length === 3 ? value.split("").map((char) => char + char).join("") : value.slice(0, 6);
const channel = (offset) => Number.parseInt(full.slice(offset, offset + 2), 16) / 255;
return [
channel(0),
channel(2),
channel(4)
];
};
const FluidGradient = memo(({ className, colors, intensity = .45, speed = 1, style, ...rest }) => {
const { isDarkMode } = useThemeMode();
const canvasRef = useRef(null);
const [ready, setReady] = useState(false);
const [canvasEpoch, setCanvasEpoch] = useState(0);
const palette = colors ?? (isDarkMode ? LANDING_PALETTE_DARK : LANDING_PALETTE_LIGHT);
const paletteRef = useRef({
colors: palette,
intensity
});
const speedRef = useRef(speed);
const repaintRef = useRef(void 0);
useEffect(() => {
paletteRef.current = {
colors: palette,
intensity
};
speedRef.current = speed;
repaintRef.current?.();
}, [
palette[0],
palette[1],
palette[2],
intensity,
speed
]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return void 0;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
let frameId = 0;
let inView = true;
let readySent = false;
let drawFrame = null;
let refreshPalette = null;
let sizeAndDraw = null;
let destroyResources = null;
const buildResources = () => {
const gl = canvas.getContext("webgl2", {
alpha: true,
antialias: false,
depth: false,
stencil: false,
powerPreference: "low-power"
});
if (!gl) return false;
const compileShader = (type, source) => {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
gl.deleteShader(shader);
return null;
}
return shader;
};
const vertexShader = compileShader(gl.VERTEX_SHADER, VERTEX_SHADER);
const fragmentShader = compileShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
if (!vertexShader || !fragmentShader) return false;
const program = gl.createProgram();
if (!program) return false;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
gl.deleteProgram(program);
return false;
}
gl.useProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1,
-1,
3,
-1,
-1,
3
]), gl.STATIC_DRAW);
const position = gl.getAttribLocation(program, "aPos");
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
const timeLocation = gl.getUniformLocation(program, "uTime");
const stopLocations = [
"uStopA",
"uStopB",
"uStopC"
].map((name) => gl.getUniformLocation(program, name));
const intensityLocation = gl.getUniformLocation(program, "uIntensity");
refreshPalette = () => {
const { colors: stops, intensity: peak } = paletteRef.current;
stops.map(hexToRgb).forEach((stop, index) => {
const location = stopLocations[index];
if (location) gl.uniform3f(location, stop[0], stop[1], stop[2]);
});
gl.uniform1f(intensityLocation, peak);
};
refreshPalette();
drawFrame = (seconds) => {
gl.uniform1f(timeLocation, seconds * speedRef.current);
gl.drawArrays(gl.TRIANGLES, 0, 3);
if (!readySent) {
readySent = true;
setReady(true);
}
};
sizeAndDraw = () => {
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
const height = Math.max(1, Math.round(canvas.clientHeight * dpr));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
gl.viewport(0, 0, width, height);
}
return canvas.clientWidth > 0 && drawFrame !== null;
};
destroyResources = (lose) => {
gl.deleteBuffer(buffer);
gl.deleteProgram(program);
drawFrame = null;
refreshPalette = null;
sizeAndDraw = null;
destroyResources = null;
if (lose) gl.getExtension("WEBGL_lose_context")?.loseContext();
};
return true;
};
if (!buildResources()) return void 0;
const renderOnce = () => {
if (!sizeAndDraw?.()) return;
drawFrame?.(performance.now() / 1e3);
};
const syncLoop = (milliseconds) => {
frameId = requestAnimationFrame(syncLoop);
if (sizeAndDraw?.()) drawFrame?.(milliseconds / 1e3);
};
const updateLoop = () => {
if (inView && !document.hidden && !reducedMotion.matches && drawFrame !== null) {
if (frameId === 0) frameId = requestAnimationFrame(syncLoop);
return;
}
if (frameId !== 0) {
cancelAnimationFrame(frameId);
frameId = 0;
}
if (!document.hidden) renderOnce();
};
const resizeObserver = new ResizeObserver(() => {
if (frameId === 0) renderOnce();
});
resizeObserver.observe(canvas);
const intersectionObserver = new IntersectionObserver(([entry]) => {
inView = entry.isIntersecting;
updateLoop();
});
intersectionObserver.observe(canvas);
const handleVisibilityChange = () => updateLoop();
document.addEventListener("visibilitychange", handleVisibilityChange);
const handleMotionPreferenceChange = () => updateLoop();
reducedMotion.addEventListener("change", handleMotionPreferenceChange);
repaintRef.current = () => {
refreshPalette?.();
if (frameId === 0) renderOnce();
};
const handleContextLost = (event) => {
event.preventDefault();
if (frameId !== 0) {
cancelAnimationFrame(frameId);
frameId = 0;
}
setCanvasEpoch((value) => value + 1);
};
canvas.addEventListener("webglcontextlost", handleContextLost);
updateLoop();
return () => {
if (frameId !== 0) cancelAnimationFrame(frameId);
frameId = 0;
resizeObserver.disconnect();
intersectionObserver.disconnect();
repaintRef.current = void 0;
document.removeEventListener("visibilitychange", handleVisibilityChange);
reducedMotion.removeEventListener("change", handleMotionPreferenceChange);
canvas.removeEventListener("webglcontextlost", handleContextLost);
if (!canvas.isConnected) destroyResources?.(true);
else destroyResources?.(false);
};
}, [canvasEpoch]);
return /* @__PURE__ */ jsx("div", {
"aria-hidden": true,
className: cx(styles.root, className),
"data-ready": ready ? "" : void 0,
style: {
"--fluid-stop-a": palette[0],
"--fluid-stop-b": palette[1],
"--fluid-stop-c": palette[2],
...style
},
...rest,
children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef }, canvasEpoch)
});
});
FluidGradient.displayName = "FluidGradient";
//#endregion
export { FluidGradient as default };
//# sourceMappingURL=FluidGradient.mjs.map