shadimage
asanshay
componentloadersource: ok · hit

Loader

Custom WebGL shader loaders with 3 shapes (sphere, swirl, ripple) × 3 styles (plain, blur, dither).

npx shadcn add "https://ds.asanshay.com/r/loader.json"
free

Visual style

glass-blur

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Files8 · source cached 10/14/2026, 12:30:34 PM

registry/new-york/blocks/loader/loader.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import { ShaderMount } from "./shader-mount";
import {
  plainFragmentShader,
  blurFragmentShader,
  ditherFragmentShader,
  hexToRgba,
} from "./shaders";

type LoaderShape = "sphere" | "swirl" | "ripple";
type LoaderStyle = "plain" | "blur" | "dither";

const sizeConfig = {
  sm: { width: 48, height: 48 },
  default: { width: 80, height: 80 },
  lg: { width: 120, height: 120 },
};

const shapeMap: Record<LoaderShape, number> = {
  sphere: 1,
  swirl: 2,
  ripple: 3,
};

const shaderMap: Record<LoaderStyle, string> = {
  plain: plainFragmentShader,
  blur: blurFragmentShader,
  dither: ditherFragmentShader,
};

interface LoaderProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Base shape of the loader */
  shape?: LoaderShape;
  /** Visual style: plain (solid), blur (soft), or dither (pixelated) */
  variant?: LoaderStyle;
  /** Size preset (maps to width/height) */
  size?: keyof typeof sizeConfig;
  /** Custom width (overrides size preset) */
  width?: number;
  /** Custom height (overrides size preset) */
  height?: number;
  /** Animation speed multiplier */
  speed?: number;
  /** Primary color */
  color?: string;
  /** Background color */
  colorBack?: string;
}

function Loader({
  shape = "sphere",
  variant = "dither",
  size = "default",
  width,
  height,
  speed = 1,
  color = "#00b3ff",
  colorBack = "transparent",
  className,
  style,
  ...props
}: LoaderProps) {
  const sizeDefaults = sizeConfig[size];
  const resolvedWidth = width ?? sizeDefaults.width;
  const resolvedHeight = height ?? sizeDefaults.height;

  const fragmentShader = shaderMap[variant];
  const shapeValue = shapeMap[shape];
  const scale = variant === "blur" ? 0.52 : 0.6;
  const usesCssVariables = color.includes("var(") || colorBack.includes("var(");
  const [cssVariableEpoch, setCssVariableEpoch] = React.useState(0);

  React.useEffect(() => {
    if (!usesCssVariables || typeof document === "undefined") {
      return;
    }

    const bumpCssVariableEpoch = () => {
      setCssVariableEpoch((value) => value + 1);
    };

    // Resolve once after mount, then re-resolve on theme/token changes.
    bumpCssVariableEpoch();

    const attributeFilter = ["class", "style", "data-theme"];
    const observer = new MutationObserver(bumpCssVariableEpoch);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter,
    });
    if (document.body) {
      observer.observe(document.body, {
        attributes: true,
        attributeFilter,
      });
    }

    const media = window.matchMedia("(prefers-color-scheme: dark)");
    if (typeof media.addEventListener === "function") {
      media.addEventListener("change", bumpCssVariableEpoch);
    } else {
      media.addListener(bumpCssVariableEpoch);
    }

    return () => {
      observer.disconnect();
      if (typeof media.removeEventListener === "function") {
        media.removeEventListener("change", bumpCssVariableEpoch);
      } else {
        media.removeListener(bumpCssVariableEpoch);
      }
    };
  }, [usesCssVariables]);

  const uniforms = React.useMemo(
    () => ({
      u_colorFront: hexToRgba(color),
      u_colorBack: hexToRgba(colorBack),
      u_shape: shapeValue,
      u_scale: scale,
      u_pxSize: 2, // Pixel size for dithering
    }),
    [color, colorBack, shapeValue, scale, cssVariableEpoch]
  );

  return (
    <div
      data-slot="loader"
      className={cn("relative overflow-hidden rounded-full", className)}
      style={{
        width: resolvedWidth,
        height: resolvedHeight,
        ...style,
      }}
      {...props}
    >
      <ShaderMount
        fragmentShader={fragmentShader}
        uniforms={uniforms}
        speed={speed}
        width={resolvedWidth}
        height={resolvedHeight}
      />
    </div>
  );
}

export { Loader, sizeConfig };
export type { LoaderProps, LoaderShape, LoaderStyle };
registry/new-york/blocks/loader/shader-mount.tsx
"use client";

import * as React from "react";
import { vertexShaderSource } from "./shaders";

interface ShaderMountProps {
  fragmentShader: string;
  uniforms: Record<string, number | number[]>;
  speed?: number;
  width: number;
  height: number;
  className?: string;
  style?: React.CSSProperties;
}

function createShader(
  gl: WebGL2RenderingContext,
  type: number,
  source: string
): WebGLShader | null {
  const shader = gl.createShader(type);
  if (!shader) return null;

  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error(
      "Shader compile error:",
      gl.getShaderInfoLog(shader)
    );
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}

function createProgram(
  gl: WebGL2RenderingContext,
  vertexSource: string,
  fragmentSource: string
): WebGLProgram | null {
  const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
  const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);

  if (!vertexShader || !fragmentShader) return null;

  const program = gl.createProgram();
  if (!program) return null;

  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error("Program link error:", gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
    return null;
  }

  // Clean up shaders after linking
  gl.detachShader(program, vertexShader);
  gl.detachShader(program, fragmentShader);
  gl.deleteShader(vertexShader);
  gl.deleteShader(fragmentShader);

  return program;
}

export function ShaderMount({
  fragmentShader,
  uniforms,
  speed = 1,
  width,
  height,
  className,
  style,
}: ShaderMountProps) {
  const canvasRef = React.useRef<HTMLCanvasElement>(null);
  const glRef = React.useRef<WebGL2RenderingContext | null>(null);
  const programRef = React.useRef<WebGLProgram | null>(null);
  const uniformLocationsRef = React.useRef<
    Record<string, WebGLUniformLocation | null>
  >({});
  const rafRef = React.useRef<number | null>(null);
  const startTimeRef = React.useRef<number>(performance.now());
  const currentSpeedRef = React.useRef<number>(speed);

  // Update speed ref when prop changes
  React.useEffect(() => {
    currentSpeedRef.current = speed;
  }, [speed]);

  // Initialize WebGL
  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const gl = canvas.getContext("webgl2", {
      alpha: true,
      premultipliedAlpha: true,
      antialias: true,
    });

    if (!gl) {
      console.error("WebGL2 not supported");
      return;
    }

    glRef.current = gl;

    // Create program
    const program = createProgram(gl, vertexShaderSource, fragmentShader);
    if (!program) return;

    programRef.current = program;

    // Setup position attribute
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positions = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

    const positionLocation = gl.getAttribLocation(program, "a_position");
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

    // Get uniform locations
    const locations: Record<string, WebGLUniformLocation | null> = {
      u_time: gl.getUniformLocation(program, "u_time"),
      u_resolution: gl.getUniformLocation(program, "u_resolution"),
      u_pixelRatio: gl.getUniformLocation(program, "u_pixelRatio"),
    };

    Object.keys(uniforms).forEach((key) => {
      locations[key] = gl.getUniformLocation(program, key);
    });

    uniformLocationsRef.current = locations;

    // Enable blending for transparency
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

    return () => {
      if (rafRef.current) {
        cancelAnimationFrame(rafRef.current);
      }
      if (gl && program) {
        gl.deleteProgram(program);
      }
    };
  }, [fragmentShader]);

  // Render loop
  React.useEffect(() => {
    const gl = glRef.current;
    const program = programRef.current;
    const locations = uniformLocationsRef.current;

    if (!gl || !program) return;

    const pixelRatio = Math.min(window.devicePixelRatio, 2);
    const canvasWidth = width * pixelRatio;
    const canvasHeight = height * pixelRatio;

    const canvas = canvasRef.current;
    if (canvas) {
      canvas.width = canvasWidth;
      canvas.height = canvasHeight;
    }

    gl.viewport(0, 0, canvasWidth, canvasHeight);

    let lastTime = performance.now();
    let accumulatedTime = 0;

    const render = (currentTime: number) => {
      const deltaTime = currentTime - lastTime;
      lastTime = currentTime;
      accumulatedTime += deltaTime * currentSpeedRef.current * 0.001;

      gl.clearColor(0, 0, 0, 0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.useProgram(program);

      // Set built-in uniforms
      if (locations.u_time) {
        gl.uniform1f(locations.u_time, accumulatedTime);
      }
      if (locations.u_resolution) {
        gl.uniform2f(locations.u_resolution, canvasWidth, canvasHeight);
      }
      if (locations.u_pixelRatio) {
        gl.uniform1f(locations.u_pixelRatio, pixelRatio);
      }

      // Set custom uniforms
      Object.entries(uniforms).forEach(([key, value]) => {
        const location = locations[key];
        if (!location) return;

        if (Array.isArray(value)) {
          switch (value.length) {
            case 2:
              gl.uniform2fv(location, value);
              break;
            case 3:
              gl.uniform3fv(location, value);
              break;
            case 4:
              gl.uniform4fv(location, value);
              break;
          }
        } else {
          gl.uniform1f(location, value);
        }
      });

      gl.drawArrays(gl.TRIANGLES, 0, 6);

      rafRef.current = requestAnimationFrame(render);
    };

    rafRef.current = requestAnimationFrame(render);

    return () => {
      if (rafRef.current) {
        cancelAnimationFrame(rafRef.current);
      }
    };
  }, [uniforms, width, height]);

  return (
    <canvas
      ref={canvasRef}
      className={className}
      style={{
        width,
        height,
        ...style,
      }}
    />
  );
}
registry/new-york/blocks/loader/shaders/index.ts
// Shader exports
export { vertexShaderSource } from "./vertex";
export { plainFragmentShader } from "./plain";
export { blurFragmentShader } from "./blur";
export { ditherFragmentShader } from "./dither";
export { hexToRgba } from "./utils";
registry/new-york/blocks/loader/shaders/vertex.ts
// Shared vertex shader for all loader styles
export const vertexShaderSource = `#version 300 es
precision mediump float;

layout(location = 0) in vec4 a_position;

void main() {
  gl_Position = a_position;
}
`;
registry/new-york/blocks/loader/shaders/utils.ts
// Shared GLSL utilities and shape functions

export const shaderConstants = `
#define TWO_PI 6.28318530718
#define PI 3.14159265358979323846
`;

export const shapeUniforms = `
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_pixelRatio;
uniform vec4 u_colorFront;
uniform vec4 u_colorBack;
uniform float u_shape; // 1=sphere, 2=swirl, 3=ripple
uniform float u_scale;
`;

// Shape functions - all shapes expect normalized UV coordinates centered at origin
export const shapeFunctions = `
// Sphere - 3D lit sphere with rotating light source
float sphereShape(vec2 uv, float time) {
  uv *= 2.0;
  float d = 1.0 - pow(length(uv), 2.0);
  vec3 pos = vec3(uv, sqrt(max(0.0, d)));
  vec3 lightPos = normalize(vec3(cos(3.0 * time), 0.8, sin(2.5 * time)));
  float shape = 0.5 + 0.5 * dot(lightPos, pos);
  return shape * step(0.0, d);
}

// Swirl - spiral vortex pattern
float swirlShape(vec2 uv, float time) {
  float l = length(uv);
  float angle = 6.0 * atan(uv.y, uv.x) + 8.0 * time;
  float twist = 1.2;
  float offset = 1.0 / pow(max(l, 1e-6), twist) + angle / TWO_PI;
  float mid = smoothstep(0.0, 1.0, pow(l, twist));
  return mix(0.0, fract(offset), mid);
}

// Ripple - concentric expanding waves
float rippleShape(vec2 uv, float time) {
  float dist = length(uv);
  float waves = sin(pow(dist, 1.7) * 7.0 - 6.0 * time) * 0.5 + 0.5;
  return waves;
}

// Get shape value based on u_shape uniform
float getShape(vec2 uv, float time) {
  if (u_shape < 1.5) {
    return sphereShape(uv, time);
  } else if (u_shape < 2.5) {
    return swirlShape(uv, time);
  } else {
    return rippleShape(uv, time);
  }
}
`;

function createTransparentRgba(): [number, number, number, number] {
  return [0, 0, 0, 0];
}

function parseHexColor(color: string): [number, number, number, number] | null {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color.trim());
  if (!result) {
    return null;
  }

  return [
    parseInt(result[1], 16) / 255,
    parseInt(result[2], 16) / 255,
    parseInt(result[3], 16) / 255,
    1.0,
  ];
}

function parseRgbColor(color: string): [number, number, number, number] | null {
  const rgbResult =
    /^rgba?\(\s*([\d.]+)(?:\s*,\s*|\s+)([\d.]+)(?:\s*,\s*|\s+)([\d.]+)(?:\s*[,/]\s*([\d.]+))?\s*\)$/i.exec(
      color.trim()
    );

  if (!rgbResult) {
    return null;
  }

  return [
    Math.min(255, Math.max(0, Number(rgbResult[1]))) / 255,
    Math.min(255, Math.max(0, Number(rgbResult[2]))) / 255,
    Math.min(255, Math.max(0, Number(rgbResult[3]))) / 255,
    rgbResult[4] ? Math.min(1, Math.max(0, Number(rgbResult[4]))) : 1.0,
  ];
}

function resolveBrowserColor(color: string): string | null {
  if (typeof window === "undefined" || typeof document === "undefined") {
    return null;
  }

  const probe = document.createElement("span");
  probe.style.color = color;

  // If the browser rejected the color value, style.color will be empty
  if (!probe.style.color) {
    return null;
  }

  probe.style.position = "absolute";
  probe.style.visibility = "hidden";
  probe.style.pointerEvents = "none";

  document.body.appendChild(probe);
  const resolvedColor = window.getComputedStyle(probe).color;
  probe.remove();

  return resolvedColor || null;
}

// Helper to convert CSS color values to RGBA array
export function hexToRgba(color: string): [number, number, number, number] {
  const normalizedColor = color.trim();

  if (!normalizedColor || normalizedColor === "transparent") {
    return createTransparentRgba();
  }

  const fromHex = parseHexColor(normalizedColor);
  if (fromHex) {
    return fromHex;
  }

  const fromRgb = parseRgbColor(normalizedColor);
  if (fromRgb) {
    return fromRgb;
  }

  const resolvedColor = resolveBrowserColor(normalizedColor);
  if (resolvedColor) {
    const resolvedFromHex = parseHexColor(resolvedColor);
    if (resolvedFromHex) {
      return resolvedFromHex;
    }

    const resolvedFromRgb = parseRgbColor(resolvedColor);
    if (resolvedFromRgb) {
      return resolvedFromRgb;
    }
  }

  return createTransparentRgba();
}
registry/new-york/blocks/loader/shaders/plain.ts
// Plain style fragment shader - crisp core with soft bloom
import { shaderConstants, shapeUniforms, shapeFunctions } from "./utils";

export const plainFragmentShader = `#version 300 es
precision mediump float;

${shaderConstants}
${shapeUniforms}

out vec4 fragColor;

${shapeFunctions}

// Gaussian-like blur by sampling shape at multiple offsets
float blurredShape(vec2 uv, float time, float blurSize) {
  float sum = 0.0;
  float totalWeight = 0.0;
  
  // 9-tap blur kernel
  for (float x = -1.0; x <= 1.0; x += 1.0) {
    for (float y = -1.0; y <= 1.0; y += 1.0) {
      vec2 offset = vec2(x, y) * blurSize;
      float weight = 1.0 - length(vec2(x, y)) * 0.3;
      sum += getShape(uv + offset, time) * weight;
      totalWeight += weight;
    }
  }
  
  return sum / totalWeight;
}

void main() {
  // Normalize coordinates to [-0.5, 0.5] centered
  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
  uv /= u_scale;
  
  float time = u_time;
  
  float core = getShape(uv, time);
  float mask = 1.0 - smoothstep(0.7, 1.05, length(uv));
  core *= mask;
  
  // Crisp core with a gentle bloom
  float solid = smoothstep(0.2, 0.85, core);
  float glow = blurredShape(uv, time, 0.018);
  float halo = smoothstep(0.0, 0.6, glow) * 0.35;
  float finalShape = clamp(solid + halo, 0.0, 1.0);
  
  // Subtle lighting for a more polished feel
  float radial = 1.0 - smoothstep(0.2, 1.05, length(uv));
  float highlight =
      pow(max(0.0, 1.0 - length(uv - vec2(-0.25, 0.25))), 3.0);
  float shade = clamp(0.75 + 0.25 * radial + 0.2 * highlight, 0.0, 1.15);
  
  // Mix colors with glow
  vec3 fgColor = u_colorFront.rgb * u_colorFront.a;
  float fgOpacity = u_colorFront.a * finalShape;
  vec3 bgColor = u_colorBack.rgb * u_colorBack.a;
  float bgOpacity = u_colorBack.a;
  
  vec3 color = fgColor * shade * finalShape;
  float opacity = fgOpacity;
  
  color += bgColor * (1.0 - opacity);
  opacity += bgOpacity * (1.0 - opacity);
  
  fragColor = vec4(color, opacity);
}
`;
registry/new-york/blocks/loader/shaders/blur.ts
// Blur style fragment shader - completely diffuse/foggy rendering
import { shaderConstants, shapeUniforms, shapeFunctions } from "./utils";

export const blurFragmentShader = `#version 300 es
precision mediump float;

${shaderConstants}
${shapeUniforms}

out vec4 fragColor;

${shapeFunctions}

// Heavy blur sampling for diffuse effect
float heavyBlur(vec2 uv, float time, float blurSize) {
  float sum = 0.0;
  float totalWeight = 0.0;
  
  // 25-tap blur kernel for heavier blur
  for (float x = -2.0; x <= 2.0; x += 1.0) {
    for (float y = -2.0; y <= 2.0; y += 1.0) {
      vec2 offset = vec2(x, y) * blurSize;
      float dist = length(vec2(x, y));
      float weight = exp(-dist * dist * 0.15); // Gaussian falloff
      sum += getShape(uv + offset, time) * weight;
      totalWeight += weight;
    }
  }
  
  return sum / totalWeight;
}

void main() {
  // Normalize coordinates to [-0.5, 0.5] centered
  vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
  uv /= u_scale;
  
  float time = u_time;
  float dist = length(uv);
  
  // Multiple heavy blur passes for completely diffuse result
  float blurAmount = 0.05;
  float shape1 = heavyBlur(uv, time, blurAmount);
  float shape2 = heavyBlur(uv, time, blurAmount * 2.5);
  float shape3 = heavyBlur(uv, time, blurAmount * 4.0);
  float shape4 = heavyBlur(uv, time, blurAmount * 6.0);
  
  // Combine blur passes - heavily weighted toward outer blur
  float shape = shape1 * 0.25 + shape2 * 0.3 + shape3 * 0.25 + shape4 * 0.2;
  
  // Very soft power curve for foggy appearance
  // Boost intensity for swirl/ripple to fill more space
  float powerCurve = (u_shape < 1.5) ? 0.6 : 0.5;
  float foggy = pow(shape, powerCurve);
  
  // Apply shape-specific edge handling with extra padding
  float edgeFade = 1.0;
  if (u_shape < 1.5) {
    // Sphere: soft radial fade with more breathing room
    edgeFade = 1.0 - smoothstep(0.55, 0.88, dist);
  } else if (u_shape < 2.5) {
    // Swirl: diffuse edges without hard cutoff
    edgeFade = 1.0 - smoothstep(0.6, 0.92, dist);
  } else {
    // Ripple: diffuse edges without hard cutoff
    edgeFade = 1.0 - smoothstep(0.6, 0.92, dist);
  }
  foggy *= edgeFade;
  
  // Extra soft smoothstep for diffuse edges
  float finalShape = smoothstep(0.0, 0.8, foggy);
  
  // Mix colors
  vec3 fgColor = u_colorFront.rgb * u_colorFront.a;
  float fgOpacity = u_colorFront.a * finalShape;
  vec3 bgColor = u_colorBack.rgb * u_colorBack.a;
  float bgOpacity = u_colorBack.a;
  
  vec3 color = fgColor * finalShape;
  float opacity = fgOpacity;
  
  color += bgColor * (1.0 - opacity);
  opacity += bgOpacity * (1.0 - opacity);
  
  fragColor = vec4(color, opacity);
}
`;
registry/new-york/blocks/loader/shaders/dither.ts
// Dither style fragment shader - Bayer matrix dithering for retro pixelated look
import { shaderConstants, shapeUniforms, shapeFunctions } from "./utils";

export const ditherFragmentShader = `#version 300 es
precision mediump float;

${shaderConstants}
${shapeUniforms}
uniform float u_pxSize;

out vec4 fragColor;

${shapeFunctions}

// 4x4 Bayer dithering matrix
const int bayer4x4[16] = int[16](
  0, 8, 2, 10,
  12, 4, 14, 6,
  3, 11, 1, 9,
  15, 7, 13, 5
);

// 8x8 Bayer dithering matrix for finer detail
const int bayer8x8[64] = int[64](
  0, 32, 8, 40, 2, 34, 10, 42,
  48, 16, 56, 24, 50, 18, 58, 26,
  12, 44, 4, 36, 14, 46, 6, 38,
  60, 28, 52, 20, 62, 30, 54, 22,
  3, 35, 11, 43, 1, 33, 9, 41,
  51, 19, 59, 27, 49, 17, 57, 25,
  15, 47, 7, 39, 13, 45, 5, 37,
  63, 31, 55, 23, 61, 29, 53, 21
);

float getBayerValue4x4(vec2 uv) {
  ivec2 pos = ivec2(mod(uv, 4.0));
  int index = pos.y * 4 + pos.x;
  return float(bayer4x4[index]) / 16.0;
}

float getBayerValue8x8(vec2 uv) {
  ivec2 pos = ivec2(mod(uv, 8.0));
  int index = pos.y * 8 + pos.x;
  return float(bayer8x8[index]) / 64.0;
}

void main() {
  float pxSize = max(2.0, u_pxSize) * u_pixelRatio;
  
  // Pixelate the UV coordinates
  vec2 pixelUV = gl_FragCoord.xy / pxSize;
  vec2 pixelatedCoord = (floor(pixelUV) + 0.5) * pxSize;
  
  // Normalize to centered coordinates
  vec2 uv = (pixelatedCoord - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
  uv /= u_scale;
  
  float time = u_time;
  float shape = getShape(uv, time);
  
  // Get Bayer dithering threshold
  float dither = getBayerValue4x4(pixelUV);
  dither -= 0.5;
  
  // Apply dithering threshold
  float result = step(0.5, shape + dither);
  
  // Mix colors
  vec3 fgColor = u_colorFront.rgb * u_colorFront.a;
  float fgOpacity = u_colorFront.a;
  vec3 bgColor = u_colorBack.rgb * u_colorBack.a;
  float bgOpacity = u_colorBack.a;
  
  vec3 color = fgColor * result;
  float opacity = fgOpacity * result;
  
  color += bgColor * (1.0 - opacity);
  opacity += bgOpacity * (1.0 - opacity);
  
  fragColor = vec4(color, opacity);
}
`;

More from asanshay

Browse all components in this library →