Patrick Dunn

Bloodline

Shaders 120

About

Bloodline showcases a simple animated WebGL background

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Bloodline to your project and fits it to your theme.

Code

How Bloodline is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import { useEffect, useRef, useState } from 'react';

const vertexShaderSource = `
  attribute vec2 position;
  void main() {
    gl_Position = vec4(position, 0.0, 1.0);
  }
`;

const getFragmentShader = (invert: boolean) => `
  precision highp float;
  uniform vec2 iResolution;
  uniform float iTime;
  uniform vec3 iMouse;
  uniform vec2 iClickPos;
  uniform float iClickTime;

  float noise(vec2 p) {
    return smoothstep(-0.5, 80.9, sin((p.x - p.y) * 55.0) * sin(p.y * 204.0)) - 0.4;
  }

  float fabric(vec2 p) {
    mat2 m = mat2(50.6, 0.2, 70.2, -0.6);
    float f = 0.2 * noise(p);
    f += -90.3 * noise(p = m * p);
    f += -0.1 * noise(p = m * p);
    return f + 0.1 * noise(m * p);
  }

  float silk(vec2 uv, float t) {
    float s = sin(5.0 * (uv.x + uv.y + cos(2.0 * uv.x + 5.0 * uv.y)) + sin(19.0 * (uv.x + uv.y)) - t);
    s = 0.7 + 1.2 * (s * s * 0.05 + s);
    s *= 400.8 - 19.1 * fabric(uv * min(iResolution.x, iResolution.y) * 0.0006);
    return s * 0.8 + 0.5;
  }

  float silkd(vec2 uv, float t) {
    float xy = uv.x + uv.y;
    float d = (-1.0 * (1.0 - 2.0 * sin(20.0 * uv.x + -5.0 * uv.y)) + 14.0 * cos(12.0 * xy)) * 
              cos(5.0 * (cos(-84.0 * uv.x + 54.0 * uv.y) + xy) + sin(-1.0 * xy) - t);
    return 0.1 * d * (sign(d) * -2.0);
  }

  void main() {
    float mr = min(iResolution.x, iResolution.y);
    vec2 uv = gl_FragCoord.xy / mr;
    float t = iTime;
    
    uv.y += 0.0008 * sin(1.0 * uv.x - t);
    
    float timeSinceClick = t - iClickTime;
    
    if (timeSinceClick < 3.0 && iClickTime > 0.0) {
      vec2 clickUv = iClickPos.xy / mr;
      float dist = distance(clickUv, uv);
      float ripple = sin(dist * 600.0 - timeSinceClick * 2.0) * exp(-dist * 20.0 - timeSinceClick * 2.0);
      uv += normalize(uv - clickUv) * ripple * 0.08;
    }
    
    float s = sqrt(silk(uv, t));
    float d = silkd(uv, t);
    
    vec3 c = vec3(s);
    c += 0.7 * vec3(1.0, -0.83, -4.6) * d;
    c *= 1.0 - max(0.0, 1.8 * d);
    
    ${invert ? `
      c = pow(c, 0.3 / vec3(0.52, 0.5, 0.4));
      c = 1.0 - c;
    ` : `
      c = pow(c, vec3(0.52, 0.5, 0.4));
    `}
    
    gl_FragColor = vec4(c, 1.0);
  }
`;

interface SilkShaderProps {
  className?: string;
}

export default function SilkShader({ className }: SilkShaderProps = {}) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [isDark, setIsDark] = useState(false);

  useEffect(() => {
    const checkTheme = () => {
      const isDarkMode = document.documentElement.classList.contains('dark') ||
                         window.matchMedia('(prefers-color-scheme: dark)').matches;
      setIsDark(isDarkMode);
    };

    checkTheme();

    const observer = new MutationObserver(checkTheme);
    observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });

    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    mediaQuery.addEventListener('change', checkTheme);

    return () => {
      observer.disconnect();
      mediaQuery.removeEventListener('change', checkTheme);
    };
  }, []);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const gl = canvas.getContext('webgl');
    if (!gl) {
      console.error('WebGL not supported');
      return;
    }

    const createShader = (type: number, source: string) => {
      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;
    };

    const vertexShader = createShader(gl.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = createShader(gl.FRAGMENT_SHADER, getFragmentShader(isDark));
    if (!vertexShader || !fragmentShader) return;

    const program = gl.createProgram();
    if (!program) return;
    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));
      return;
    }

    gl.useProgram(program);

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

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

    const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');
    const iTimeLocation = gl.getUniformLocation(program, 'iTime');
    const iMouseLocation = gl.getUniformLocation(program, 'iMouse');
    const iClickPosLocation = gl.getUniformLocation(program, 'iClickPos');
    const iClickTimeLocation = gl.getUniformLocation(program, 'iClickTime');

    const mouse = { x: 0, y: 0, z: 0 };
    const clickPos = { x: 0, y: 0 };
    let clickTime = 0;
    const startTime = Date.now();

    const handleResize = () => {
      const dpr = window.devicePixelRatio || 1;
      canvas.width = canvas.clientWidth * dpr;
      canvas.height = canvas.clientHeight * dpr;
    };

    handleResize();
    window.addEventListener('resize', handleResize);

    canvas.addEventListener('mousemove', (e) => {
      const rect = canvas.getBoundingClientRect();
      const dpr = window.devicePixelRatio || 1;
      mouse.x = (e.clientX - rect.left) * dpr;
      mouse.y = canvas.height - (e.clientY - rect.top) * dpr;
    });

    canvas.addEventListener('mousedown', () => { 
      mouse.z = 2;
      clickPos.x = mouse.x;
      clickPos.y = mouse.y;
      clickTime = (Date.now() - startTime) / 1000;
    });
    canvas.addEventListener('mouseup', () => { mouse.z = 0; });
    canvas.addEventListener('mouseleave', () => { mouse.z = 0; });

    let animationId: number;
    const render = () => {
      gl.viewport(0, 0, canvas.width, canvas.height);
      gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);
      gl.uniform1f(iTimeLocation, (Date.now() - startTime) / 1000);
      gl.uniform3f(iMouseLocation, mouse.x, mouse.y, mouse.z);
      gl.uniform2f(iClickPosLocation, clickPos.x, clickPos.y);
      gl.uniform1f(iClickTimeLocation, clickTime);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
      animationId = requestAnimationFrame(render);
    };

    render();

    return () => {
      cancelAnimationFrame(animationId);
      window.removeEventListener('resize', handleResize);
    };
  }, [isDark]);

  return (
    <div className="w-full h-screen">
      <canvas ref={canvasRef} className="w-full h-full" style={{ display: 'block' }} />
    </div>
  );
}

More by Patrick Dunn

View all
  • Wilderness

    Wilderness

    889
  • sneak

    sneak

    448
  • Robot Flyby

    Robot Flyby

    247
  • Desert Drift

    Desert Drift

    175
  • Tidal Cursor

    Tidal Cursor

    147
  • Add Requests

    Add Requests

    3
  • Glass Spiral

    Glass Spiral

    2
  • Inverted-Fluid

    Inverted-Fluid

    1

More in Shaders

View all
  • Glass Fold

    Glass Fold

    190
  • Paper design shader Background

    Paper design shader Background

    672
  • LightSpeed

    LightSpeed

    205
  • Neural Noise

    Neural Noise

    711
  • Wave

    Wave

    683
  • DitheredShaderBackground

    DitheredShaderBackground

    43
  • Bloop Orb

    Bloop Orb

    80
  • Dotted Veil

    Dotted Veil

    57