Globe
aadmin2026-09-16 发布3D6 阅读

Globe

类型:3D · 分类:Backgrounds · 收录:2026-09-16 20:35:01 · 源码:会员可见

[hidecontent type="vip1"]
# Globe
The original layered FBM energy sphere with translucent rim glow and depth-aware star field.

> 来源:ThreeUI Community · https://threeui.com · MIT License(Meng To / Design+Code)
> 组件页:https://threeui.com/component/energy-orb · 预览动画:https://threeui.com/previews/energy-orb.webm
> 分类:Backgrounds

/* ===== src/shaders/globe/GlobeCollection.tsx (tsx) ===== */
import { lazy, Suspense } from "react";
import type { EnergyOrbProps } from "../energy-orb/EnergyOrb";
export type GlobeVariant = "energy-orb";
export type GlobeCollectionProps = EnergyOrbProps & { variant?: "energy-orb" };
const EnergyOrbVariant = lazy(() => import("../energy-orb/EnergyOrb").then((module) => ({ default: module.EnergyOrb })));
export function GlobeCollection({ variant: _variant, ...props }: GlobeCollectionProps) {
  return <Suspense fallback={<div className="threeui-background energy-orb" style={{ background: "#05030e" }} />}><EnergyOrbVariant {...props} /></Suspense>;
}

/* ===== src/shaders/energy-orb/EnergyOrb.tsx (tsx) ===== */
import { useEffect, useRef } from "react";
import { NXA_ENERGY_ORB_CONFIGURABLE_FRAGMENT_SHADER, NXA_ENERGY_ORB_VERTEX_SHADER } from "./energyOrbShaders";

export type EnergyOrbProps = {
  speed?: number;
  scale?: number;
  smokeScale?: number;
  smokeStrength?: number;
  smokeSpeed?: number;
  hue?: number;
  saturation?: number;
  glow?: number;
  starDensity?: number;
  starSpeed?: number;
  starSize?: number;
  brightness?: number;
  opacity?: number;
  className?: string;
};

export const ENERGY_ORB_DEFAULTS = {
  speed: 1,
  scale: 1,
  smokeScale: 1,
  smokeStrength: 1,
  smokeSpeed: 1,
  hue: 0,
  saturation: 1,
  glow: 1,
  starDensity: 1,
  starSpeed: 1,
  starSize: 1,
  brightness: 1,
  opacity: 1,
} as const;

type Star = { x: number; y: number; depth: number; phase: number; drift: number; size: number };

function compile(gl: WebGLRenderingContext, type: number, source: string) {
  const shader = gl.createShader(type);
  if (!shader) throw new Error("Unable to create energy-orb shader");
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const message = gl.getShaderInfoLog(shader) ?? "Energy orb shader compilation failed";
    console.error(message);
    throw new Error(message);
  }
  return shader;
}

function seeded(index: number, salt: number) {
  return Math.abs(Math.sin(index * 91.173 + salt * 17.719) * 43758.5453) % 1;
}

function createStars(count: number): Star[] {
  return Array.from({ length: count }, (_, index) => ({
    x: seeded(index, 1),
    y: seeded(index, 2),
    depth: 0.25 + seeded(index, 3) * 0.75,
    phase: seeded(index, 4) * Math.PI * 2,
    drift: 0.35 + seeded(index, 5) * 0.65,
    size: 0.45 + seeded(index, 6) * 1.15,
  }));
}

function fract(value: number) {
  return value - Math.floor(value);
}

export function EnergyOrb({ className = "", ...props }: EnergyOrbProps) {
  const hostRef = useRef<HTMLDivElement>(null);
  const starCanvasRef = useRef<HTMLCanvasElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const optionsRef = useRef({ ...ENERGY_ORB_DEFAULTS, ...props });
  optionsRef.current = { ...ENERGY_ORB_DEFAULTS, ...props };

  useEffect(() => {
    const host = hostRef.current;
    const starCanvas = starCanvasRef.current;
    const canvas = canvasRef.current;
    if (!host || !starCanvas || !canvas) return undefined;

    const starContext = starCanvas.getContext("2d", { alpha: true });
    const gl = canvas.getContext("webgl", { alpha: true, premultipliedAlpha: false, antialias: true });
    if (!starContext || !gl) return undefined;

    const vertex = compile(gl, gl.VERTEX_SHADER, NXA_ENERGY_ORB_VERTEX_SHADER);
    const fragment = compile(gl, gl.FRAGMENT_SHADER, NXA_ENERGY_ORB_CONFIGURABLE_FRAGMENT_SHADER);
    const program = gl.createProgram();
    if (!program) {
      gl.deleteShader(vertex);
      gl.deleteShader(fragment);
      return undefined;
    }
    gl.attachShader(program, vertex);
    gl.attachShader(program, fragment);
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      const message = gl.getProgramInfoLog(program) ?? "Energy orb program link failed";
      console.error(message);
      throw new Error(message);
    }
    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, "p");
    gl.enableVertexAttribArray(position);
    gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);

    const uniforms = {
      time: gl.getUniformLocation(program, "uT"),
      resolution: gl.getUniformLocation(program, "uR"),
      smokeScale: gl.getUniformLocation(program, "uSmokeScale"),
      smokeStrength: gl.getUniformLocation(program, "uSmokeStrength"),
      smokeSpeed: gl.getUniformLocation(program, "uSmokeSpeed"),
      hue: gl.getUniformLocation(program, "uHue"),
      saturation: gl.getUniformLocation(program, "uSaturation"),
      glow: gl.getUniformLocation(program, "uGlow"),
    };
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.clearColor(0, 0, 0, 0);

    const stars = createStars(180);
    const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
    let reducedMotion = motionQuery.matches;
    let width = 1;
    let height = 1;
    let starDpr = 1;
    let frame = 0;
    let visible = true;
    const startedAt = performance.now();

    const resize = () => {
      const bounds = host.getBoundingClientRect();
      width = Math.max(1, bounds.width);
      height = Math.max(1, bounds.height);
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      const bufferWidth = Math.max(1, Math.round(width * dpr));
      const bufferHeight = Math.max(1, Math.round(height * dpr));
      if (canvas.width !== bufferWidth || canvas.height !== bufferHeight) {
        canvas.width = bufferWidth;
        canvas.height = bufferHeight;
      }
      gl.viewport(0, 0, bufferWidth, bufferHeight);
      gl.uniform2f(uniforms.resolution, bufferWidth, bufferHeight);

      starDpr = Math.min(window.devicePixelRatio || 1, 1.5);
      const starWidth = Math.max(1, Math.round(width * starDpr));
      const starHeight = Math.max(1, Math.round(height * starDpr));
      if (starCanvas.width !== starWidth || starCanvas.height !== starHeight) {
        starCanvas.width = starWidth;
        starCanvas.height = starHeight;
      }
    };

    const drawStars = (elapsed: number) => {
      const options = optionsRef.current;
      starContext.setTransform(1, 0, 0, 1, 0, 0);
      starContext.clearRect(0, 0, starCanvas.width, starCanvas.height);
      const density = Math.max(0, options.starDensity);
      const count = Math.min(stars.length, Math.round((width * height / 4_200) * density));
      if (!count) return;

      starContext.setTransform(starDpr, 0, 0, starDpr, 0, 0);
      starContext.globalCompositeOperation = "screen";
      const particleTime = reducedMotion ? 0 : elapsed * Math.max(0, options.starSpeed);
      const colorHue = fract((252 + options.hue) / 360) * 360;
      for (let index = 0; index < count; index += 1) {
        const star = stars[index];
        const x = fract(star.x + particleTime * 0.0022 * star.drift) * width;
        const y = fract(star.y - particleTime * 0.0008 * star.depth + 1) * height;
        const twinkle = reducedMotion ? 0.78 : 0.58 + Math.sin(particleTime * (0.8 + star.depth) + star.phase) * 0.24;
        const alpha = Math.max(0.08, twinkle * (0.22 + star.depth * 0.48));
        const radius = Math.max(0.35, star.size * star.depth * Math.max(0.25, options.starSize));
        starContext.fillStyle = `hsla(${colorHue}, 84%, ${72 + star.depth * 20}%, ${alpha})`;
        starContext.beginPath();
        starContext.arc(x, y, radius, 0, Math.PI * 2);
        starContext.fill();
      }
      starContext.globalCompositeOperation = "source-over";
    };

    const render = (now: number) => {
      frame = 0;
      const options = optionsRef.current;
      const elapsed = (now - startedAt) * 0.001;
      drawStars(elapsed);
      gl.uniform1f(uniforms.time, elapsed * options.speed);
      gl.uniform1f(uniforms.smokeScale, Math.max(0.01, options.smokeScale));
      gl.uniform1f(uniforms.smokeStrength, Math.max(0, options.smokeStrength));
      gl.uniform1f(uniforms.smokeSpeed, Math.max(0, options.smokeSpeed));
      gl.uniform1f(uniforms.hue, options.hue * Math.PI / 180);
      gl.uniform1f(uniforms.saturation, Math.max(0, options.saturation));
      gl.uniform1f(uniforms.glow, Math.max(0, options.glow));
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
      if (visible && !document.hidden) frame = requestAnimationFrame(render);
    };

    const start = () => {
      if (!frame && visible && !document.hidden) frame = requestAnimationFrame(render);
    };
    const stop = () => {
      if (frame) cancelAnimationFrame(frame);
      frame = 0;
    };
    const onVisibilityChange = () => {
      if (document.hidden) stop();
      else start();
    };
    const onMotionChange = (event: MediaQueryListEvent) => {
      reducedMotion = event.matches;
    };

    const resizeObserver = new ResizeObserver(resize);
    const intersection = new IntersectionObserver(([entry]) => {
      visible = entry?.isIntersecting ?? true;
      if (visible) start();
      else stop();
    });
    resizeObserver.observe(host);
    intersection.observe(host);
    document.addEventListener("visibilitychange", onVisibilityChange);
    motionQuery.addEventListener("change", onMotionChange);
    resize();
    start();

    return () => {
      stop();
      resizeObserver.disconnect();
      intersection.disconnect();
      document.removeEventListener("visibilitychange", onVisibilityChange);
      motionQuery.removeEventListener("change", onMotionChange);
      gl.deleteBuffer(buffer);
      gl.deleteShader(vertex);
      gl.deleteShader(fragment);
      gl.deleteProgram(program);
    };
  }, []);

  const options = optionsRef.current;
  return (
    <div ref={hostRef} className={`threeui-background energy-orb${className ? ` ${className}` : ""}`} style={{ background: "#05030e" }}>
      <canvas ref={starCanvasRef} className="energy-orb__stars" aria-hidden="true" style={{ zIndex: 0, pointerEvents: "none" }} />
      <canvas ref={canvasRef} className="energy-orb__shader" aria-hidden="true" style={{ zIndex: 1, opacity: options.opacity, filter: `brightness(${options.brightness})`, transform: `scale(${options.scale})`, pointerEvents: "none" }} />
    </div>
  );
}

/* ===== src/shaders/energy-orb/energyOrbShaders.ts (typescript) ===== */
export const NXA_ENERGY_ORB_VERTEX_SHADER = "attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}";

export const NXA_ENERGY_ORB_FRAGMENT_SHADER = [
  "precision highp float;",
  "uniform float uT;uniform vec2 uR;",
  "float hash(vec3 p){p=fract(p*0.3183099+vec3(0.1,0.2,0.3));p*=17.0;return fract(p.x*p.y*p.z*(p.x+p.y+p.z));}",
  "float noise(vec3 x){vec3 i=floor(x);vec3 f=fract(x);f=f*f*(3.0-2.0*f);",
  " return mix(mix(mix(hash(i),hash(i+vec3(1,0,0)),f.x),mix(hash(i+vec3(0,1,0)),hash(i+vec3(1,1,0)),f.x),f.y),",
  " mix(mix(hash(i+vec3(0,0,1)),hash(i+vec3(1,0,1)),f.x),mix(hash(i+vec3(0,1,1)),hash(i+vec3(1,1,1)),f.x),f.y),f.z);}",
  "float fbm(vec3 p){float v=0.0;float a=0.5;for(int i=0;i<5;i++){v+=a*noise(p);p=p*2.03+vec3(1.7);a*=0.5;}return v;}",
  "void main(){",
  " vec2 uv=(gl_FragCoord.xy-0.5*uR)/min(uR.x,uR.y);",
  " float r=length(uv);",
  " float R=0.31;",
  " vec3 col=vec3(0.0);float alpha=0.0;",
  " if(r<R){",
  "   float z=sqrt(R*R-r*r);",
  "   vec3 n=normalize(vec3(uv,z));",
  "   float ca=uT*0.15;",
  "   mat3 rot=mat3(cos(ca),0.,sin(ca),0.,1.,0.,-sin(ca),0.,cos(ca));",
  "   vec3 sp=rot*n;",
  "   float f1=fbm(sp*2.6+vec3(0.0,uT*0.12,0.0));",
  "   float f2=fbm(sp*4.5-vec3(uT*0.08,0.0,uT*0.05)+f1*1.8);",
  "   float veil=smoothstep(0.35,0.75,f2);",
  "   vec3 deep=vec3(0.04,0.02,0.12);",
  "   vec3 mid=vec3(0.22,0.16,0.55);",
  "   vec3 bright=vec3(0.62,0.60,0.98);",
  "   col=mix(deep,mid,f1*1.2);",
  "   col=mix(col,bright,veil*0.65);",
  "   float fres=pow(1.0-z/R,2.2);",
  "   col+=vec3(0.55,0.55,1.0)*fres*1.1;",
  "   float top=pow(max(dot(n,normalize(vec3(0.0,0.7,0.7))),0.0),3.0);",
  "   col+=vec3(0.45,0.42,0.9)*top*0.35;",
  "   alpha=1.0;",
  " }",
  " float glow=exp(-(r-R)*14.0);",
  " if(r>=R){",
  "   glow=clamp(glow,0.0,1.0);",
  "   col=vec3(0.55,0.52,1.0)*glow*0.8;",
  "   alpha=glow*0.85;",
  " } else {",
  "   float rim=smoothstep(R-0.03,R,r);",
  "   col+=vec3(0.6,0.58,1.0)*rim*0.6;",
  " }",
  " gl_FragColor=vec4(col,alpha);",
  "}",
].join("n");

// The canonical source above remains byte-for-byte intact for provenance checks.
// This host variant replaces only source constants with uniforms whose defaults
// reproduce the authored rendering exactly.
export const NXA_ENERGY_ORB_CONFIGURABLE_FRAGMENT_SHADER = [
  "precision highp float;",
  "uniform float uT;uniform vec2 uR;",
  "uniform float uSmokeScale;uniform float uSmokeStrength;uniform float uSmokeSpeed;",
  "uniform float uHue;uniform float uSaturation;uniform float uGlow;",
  "float hash(vec3 p){p=fract(p*0.3183099+vec3(0.1,0.2,0.3));p*=17.0;return fract(p.x*p.y*p.z*(p.x+p.y+p.z));}",
  "float noise(vec3 x){vec3 i=floor(x);vec3 f=fract(x);f=f*f*(3.0-2.0*f);",
  " return mix(mix(mix(hash(i),hash(i+vec3(1,0,0)),f.x),mix(hash(i+vec3(0,1,0)),hash(i+vec3(1,1,0)),f.x),f.y),",
  " mix(mix(hash(i+vec3(0,0,1)),hash(i+vec3(1,0,1)),f.x),mix(hash(i+vec3(0,1,1)),hash(i+vec3(1,1,1)),f.x),f.y),f.z);}",
  "float fbm(vec3 p){float v=0.0;float a=0.5;for(int i=0;i<5;i++){v+=a*noise(p);p=p*2.03+vec3(1.7);a*=0.5;}return v;}",
  "vec3 gradeColor(vec3 color){",
  " float luminance=dot(color,vec3(0.2126,0.7152,0.0722));",
  " color=mix(vec3(luminance),color,uSaturation);",
  " vec3 axis=normalize(vec3(1.0));",
  " return max(vec3(0.0),color*cos(uHue)+cross(axis,color)*sin(uHue)+axis*dot(axis,color)*(1.0-cos(uHue)));",
  "}",
  "void main(){",
  " vec2 uv=(gl_FragCoord.xy-0.5*uR)/min(uR.x,uR.y);",
  " float r=length(uv);",
  " float R=0.31;",
  " vec3 col=vec3(0.0);float alpha=0.0;",
  " if(r<R){",
  "   float z=sqrt(R*R-r*r);",
  "   vec3 n=normalize(vec3(uv,z));",
  "   float ca=uT*0.15;",
  "   mat3 rot=mat3(cos(ca),0.,sin(ca),0.,1.,0.,-sin(ca),0.,cos(ca));",
  "   vec3 sp=rot*n;",
  "   float smokeTime=uT*uSmokeSpeed;",
  "   float f1=fbm(sp*(2.6*uSmokeScale)+vec3(0.0,smokeTime*0.12,0.0));",
  "   float f2=fbm(sp*(4.5*uSmokeScale)-vec3(smokeTime*0.08,0.0,smokeTime*0.05)+f1*1.8);",
  "   float veil=smoothstep(0.35,0.75,f2);",
  "   vec3 deep=vec3(0.04,0.02,0.12);",
  "   vec3 mid=vec3(0.22,0.16,0.55);",
  "   vec3 bright=vec3(0.62,0.60,0.98);",
  "   col=mix(deep,mid,f1*1.2);",
  "   col=mix(col,bright,clamp(veil*0.65*uSmokeStrength,0.0,1.0));",
  "   float fres=pow(1.0-z/R,2.2);",
  "   col+=vec3(0.55,0.55,1.0)*fres*1.1*uGlow;",
  "   float top=pow(max(dot(n,normalize(vec3(0.0,0.7,0.7))),0.0),3.0);",
  "   col+=vec3(0.45,0.42,0.9)*top*0.35*uGlow;",
  "   alpha=1.0;",
  " }",
  " float glow=exp(-(r-R)*14.0);",
  " if(r>=R){",
  "   glow=clamp(glow,0.0,1.0);",
  "   col=vec3(0.55,0.52,1.0)*glow*0.8*uGlow;",
  "   alpha=glow*0.85;",
  " } else {",
  "   float rim=smoothstep(R-0.03,R,r);",
  "   col+=vec3(0.6,0.58,1.0)*rim*0.6*uGlow;",
  " }",
  " col=gradeColor(col);",
  " gl_FragColor=vec4(col,alpha);",
  "}",
].join("n");
[/hidecontent]
← 返回首页