Todos os componentes

Sdf Blob Transition

Sdf Blob Transition é um componente para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.

SmoothUI MIT other

O que é

Sdf Blob Transition é um componente para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Precisa de

npm i motion

Licença

Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Sdf Blob Transition

index.tsx
"use client";

import { cn } from "@/lib/utils";
import { useReducedMotion } from "motion/react";
import { type ReactNode, useEffect, useRef, useState } from "react";

export type SdfBlobVariant = "circle" | "warped" | "merge" | "radial" | "final";

export interface SdfBlobTransitionProps {
  children: ReactNode;
  className?: string;
  duration?: number;
  onRest?: () => void;
  transitionKey: string | number;
  variant?: SdfBlobVariant;
}

const DEFAULT_DURATION_MS = 1120;
const MIDPOINT = 0.5;
const MAX_DPR = 2;
const VERTEX_SHADER =
  "attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }";
const FRAGMENT_SHADER = `
precision mediump float;
uniform vec2 uRes;
uniform float uTime;
uniform float uProgress;
uniform float uAlpha;
uniform float uVariant;
#define PI 3.14159265359

float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
float noise(vec2 p){
  vec2 i = floor(p);
  vec2 f = fract(p);
  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));
  vec2 u = f * f * (3.0 - 2.0 * f);
  return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

float circleSdf(vec2 p, vec2 c, float r){ return length(p - c) - r; }
float smin(float a, float b, float k){
  float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
  return mix(b, a, h) - k * h * (1.0 - h);
}

void main(){
  vec2 uv = gl_FragCoord.xy / uRes;
  vec2 aspect = vec2(uRes.x / uRes.y, 1.0);
  vec2 p = (uv - 0.5) * aspect;
  float progress = smoothstep(0.0, 1.0, uProgress);
  float grow = smoothstep(0.0, 0.86, progress);
  float exitFade = smoothstep(1.0, 0.72, progress);

  float wobble = noise(uv * 7.0 + uTime * 0.18) * 0.035;
  float ringNoise = noise(vec2(atan(p.y, p.x) * 2.0, uTime * 0.18)) * 0.075;
  float dCircle = circleSdf(p, vec2(0.0), 0.04 + grow * 0.64);
  float dWarped = circleSdf(p, vec2(0.0), 0.04 + grow * 0.62 + ringNoise * smoothstep(0.08, 0.96, progress));

  float m0 = circleSdf(p, vec2(-0.26, -0.08), 0.06 + grow * 0.42 + wobble);
  float m1 = circleSdf(p, vec2(0.24, 0.12), 0.06 + grow * 0.40 - wobble * 0.5);
  float dMerge = smin(m0, m1, 0.18);

  float angle = atan(p.y, p.x);
  float sector3 = 2.0 * PI / 3.0;
  float sector5 = 2.0 * PI / 5.0;
  float a3 = floor((angle + PI) / sector3 + 0.5) * sector3 - PI;
  float a5 = floor((angle + PI) / sector5 + 0.5) * sector5 - PI;
  float r0 = 0.17 + grow * 0.24;
  float r1 = 0.30 + grow * 0.18;
  float dR0 = circleSdf(p, vec2(cos(a3), sin(a3)) * r0, 0.04 + grow * 0.22 + wobble * 0.4);
  float dR1 = circleSdf(p, vec2(cos(a5), sin(a5)) * r1, 0.035 + grow * 0.18);
  float dRadial = smin(dR0, dR1, 0.14);

  float f0 = circleSdf(p, vec2(-0.28, -0.10), 0.08 + grow * 0.46 + wobble);
  float f1 = circleSdf(p, vec2(0.00, 0.16), 0.06 + grow * 0.42 - wobble * 0.5);
  float f2 = circleSdf(p, vec2(0.30, -0.06), 0.05 + grow * 0.40 + wobble * 0.6);
  float f3 = circleSdf(p, vec2(-0.02, -0.30), 0.04 + grow * 0.34);
  float dFinal = smin(smin(f0, f1, 0.18), smin(f2, f3, 0.16), 0.20);
  dFinal = smin(dFinal, dRadial, 0.16);

  float d = dFinal;
  if (uVariant < 0.5) {
    d = dCircle;
  } else if (uVariant < 1.5) {
    d = dWarped;
  } else if (uVariant < 2.5) {
    d = dMerge;
  } else if (uVariant < 3.5) {
    d = dRadial;
  }

  float edge = exp(-d * d * 160.0) * exitFade;
  float fill = smoothstep(0.08, -0.10, d) * smoothstep(0.0, 0.18, progress) * exitFade;
  float cell = hash(floor((uv + noise(uv * 11.0) * 0.01) * uRes / 7.0) + floor(uTime * 10.0));
  float flecks = step(0.78, cell) * edge * 0.22 * smoothstep(1.5, 4.0, uVariant);

  vec3 rose = vec3(1.0, 0.23, 0.58);
  vec3 apricot = vec3(1.0, 0.70, 0.38);
  vec3 mint = vec3(0.42, 0.94, 0.78);
  vec3 color = mix(rose, apricot, smoothstep(-0.35, 0.35, p.x + noise(uv * 5.0) * 0.18));
  color = mix(color, mint, smoothstep(0.12, 0.7, p.y + grow * 0.16));

  float alpha = (fill * 0.24 + edge * 0.72 + flecks) * uAlpha;
  gl_FragColor = vec4(color * alpha + vec3(1.0) * edge * 0.18 * uAlpha, clamp(alpha, 0.0, 0.9));
}
`;
const STRIP = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);

interface Playback {
  cancel: () => void;
}
interface Controller {
  destroy: () => void;
  setAlpha: (alpha: number) => void;
  setProgress: (progress: number) => void;
  setVariant: (variant: SdfBlobVariant) => void;
}

const clamp01 = (value: number) => Math.max(0, Math.min(1, value));
const easeOutQuart = (value: number) => 1 - (1 - value) ** 4;
const easeInOutCubic = (value: number) =>
  value < 0.5 ? 4 * value ** 3 : 1 - (-2 * value + 2) ** 3 / 2;

const compile = (gl: WebGLRenderingContext, 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)) {
    gl.deleteShader(shader);
    return null;
  }
  return shader;
};

const resize = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {
  const rect = canvas.getBoundingClientRect();
  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
  const width = Math.max(1, Math.round(rect.width * dpr));
  const height = Math.max(1, Math.round(rect.height * dpr));
  if (canvas.width !== width || canvas.height !== height) {
    canvas.width = width;
    canvas.height = height;
  }
  gl.viewport(0, 0, width, height);
};

const variantToUniform = (variant: SdfBlobVariant) => {
  switch (variant) {
    case "circle":
      return 0;
    case "warped":
      return 1;
    case "merge":
      return 2;
    case "radial":
      return 3;
    default:
      return 4;
  }
};

const createController = (
  canvas: HTMLCanvasElement,
  initialVariant: SdfBlobVariant
): Controller | null => {
  const gl = canvas.getContext("webgl", {
    alpha: true,
    antialias: false,
    premultipliedAlpha: true,
  });
  if (!gl) {
    return null;
  }
  const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
  const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
  const program = gl.createProgram();
  if (!(vertex && fragment && program)) {
    return null;
  }
  gl.attachShader(program, vertex);
  gl.attachShader(program, fragment);
  gl.linkProgram(program);
  gl.deleteShader(vertex);
  gl.deleteShader(fragment);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    gl.deleteProgram(program);
    return null;
  }

  // biome-ignore lint/correctness/useHookAtTopLevel: WebGLRenderingContext.useProgram is not a React hook.
  gl.useProgram(program);
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, STRIP, gl.STATIC_DRAW);
  const position = gl.getAttribLocation(program, "a");
  gl.enableVertexAttribArray(position);
  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

  const uRes = gl.getUniformLocation(program, "uRes");
  const uTime = gl.getUniformLocation(program, "uTime");
  const uProgress = gl.getUniformLocation(program, "uProgress");
  const uAlpha = gl.getUniformLocation(program, "uAlpha");
  const uVariant = gl.getUniformLocation(program, "uVariant");
  const startedAt = performance.now();
  let progress = 0;
  let alpha = 0;
  let variant = initialVariant;
  let frame = 0;
  let destroyed = false;

  const tick = () => {
    if (destroyed) {
      return;
    }
    resize(canvas, gl);
    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.uniform2f(uRes, canvas.width, canvas.height);
    gl.uniform1f(uTime, (performance.now() - startedAt) / 1000);
    gl.uniform1f(uProgress, progress);
    gl.uniform1f(uAlpha, alpha);
    gl.uniform1f(uVariant, variantToUniform(variant));
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    frame = requestAnimationFrame(tick);
  };
  frame = requestAnimationFrame(tick);

  return {
    destroy: () => {
      destroyed = true;
      cancelAnimationFrame(frame);
      gl.clear(gl.COLOR_BUFFER_BIT);
      if (buffer) {
        gl.deleteBuffer(buffer);
      }
      gl.deleteProgram(program);
    },
    setAlpha: (nextAlpha: number) => {
      alpha = clamp01(nextAlpha);
    },
    setProgress: (nextProgress: number) => {
      progress = clamp01(nextProgress);
    },
    setVariant: (nextVariant: SdfBlobVariant) => {
      variant = nextVariant;
    },
  };
};

const play = (
  controller: Controller,
  duration: number,
  onMidpoint: () => void,
  onComplete: () => void
): Playback => {
  let frame = 0;
  let cancelled = false;
  let didSwap = false;
  const startedAt = performance.now();
  controller.setAlpha(1);

  const advance = () => {
    if (cancelled) {
      return;
    }
    const raw = clamp01((performance.now() - startedAt) / duration);
    const progress = easeOutQuart(raw);
    controller.setProgress(progress);
    if (!(didSwap || progress < MIDPOINT)) {
      didSwap = true;
      onMidpoint();
    }
    if (raw < 1) {
      frame = requestAnimationFrame(advance);
      return;
    }
    if (!didSwap) {
      onMidpoint();
    }
    const fadeStartedAt = performance.now();
    const fade = () => {
      if (cancelled) {
        return;
      }
      const fadeRaw = clamp01((performance.now() - fadeStartedAt) / 90);
      controller.setAlpha(1 - easeInOutCubic(fadeRaw));
      if (fadeRaw < 1) {
        frame = requestAnimationFrame(fade);
        return;
      }
      controller.setAlpha(0);
      controller.setProgress(0);
      onComplete();
    };
    frame = requestAnimationFrame(fade);
  };
  frame = requestAnimationFrame(advance);

  return {
    cancel: () => {
      cancelled = true;
      cancelAnimationFrame(frame);
    },
  };
};

const SdfBlobTransition = ({
  children,
  className,
  duration = DEFAULT_DURATION_MS,
  onRest,
  transitionKey,
  variant = "final",
}: SdfBlobTransitionProps) => {
  const shouldReduceMotion = useReducedMotion();
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const controllerRef = useRef<Controller | null>(null);
  const playbackRef = useRef<Playback | null>(null);
  const previousKey = useRef(transitionKey);
  const [renderedChildren, setRenderedChildren] = useState(children);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }
    const controller = createController(canvas, variant);
    controllerRef.current = controller;
    return () => {
      playbackRef.current?.cancel();
      controller?.destroy();
      controllerRef.current = null;
    };
  }, [variant]);

  useEffect(() => {
    if (previousKey.current === transitionKey) {
      setRenderedChildren(children);
      return;
    }
    previousKey.current = transitionKey;
    playbackRef.current?.cancel();

    if (shouldReduceMotion) {
      setRenderedChildren(children);
      setIsActive(false);
      onRest?.();
      return;
    }

    const controller = controllerRef.current;
    if (!controller) {
      setRenderedChildren(children);
      onRest?.();
      return;
    }
    controller.setVariant(variant);
    setIsActive(true);
    playbackRef.current = play(
      controller,
      duration,
      () => setRenderedChildren(children),
      () => {
        setIsActive(false);
        playbackRef.current = null;
        onRest?.();
      }
    );
  }, [children, duration, onRest, shouldReduceMotion, transitionKey, variant]);

  return (
    <div
      className={cn(
        "relative isolate overflow-hidden rounded-2xl bg-background text-foreground",
        className
      )}
      data-transitioning={isActive ? "true" : "false"}
    >
      <div className="relative z-0">{renderedChildren}</div>
      <canvas
        className={cn(
          "pointer-events-none absolute inset-0 z-10 h-full w-full transition-opacity duration-75",
          isActive && !shouldReduceMotion ? "opacity-100" : "opacity-0"
        )}
        ref={canvasRef}
      />
    </div>
  );
};

export default SdfBlobTransition;

Componentes parecidos