Todos os componentes

Prism Sweep Transition

Prism Sweep 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 é

Prism Sweep 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.

Prism Sweep Transition

index.tsx
"use client";

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

export type PrismSweepDirection = "left" | "right" | "up" | "down";
export type PrismSweepTone = "prism" | "chroma" | "aperture" | "beam";

export interface PrismSweepTransitionProps {
  /** Content rendered underneath the sweep. */
  children: ReactNode;
  /** Optional wrapper className. */
  className?: string;
  /** Direction the prism field travels from. */
  direction?: PrismSweepDirection;
  /** Total sweep duration, in milliseconds. */
  duration?: number;
  /** Called after the sweep finishes. */
  onRest?: () => void;
  /** Optional tone preset for the shader palette and field behavior. */
  tone?: PrismSweepTone;
  /** Key that starts a new sweep when it changes. */
  transitionKey: string | number;
}

const DEFAULT_DURATION_MS = 1240;
const OUTRO_MS = 82;
const CONTENT_SWAP_RATIO = 0.52;
const MAX_DEVICE_PIXEL_RATIO = 2;
const MIN_PHASE_MS = 80;
const HIDDEN_OPACITY = 0;
const VISIBLE_OPACITY = 1;

const VERTEX_SHADER = `
attribute vec2 position;
varying vec2 vUv;

void main() {
  vUv = position * 0.5 + 0.5;
  gl_Position = vec4(position, 0.0, 1.0);
}
`;

const FRAGMENT_SHADER = `
precision mediump float;

varying vec2 vUv;

uniform vec2 uResolution;
uniform float uClock;
uniform float uTravel;
uniform float uOpacity;
uniform float uAxis;
uniform float uForward;
uniform float uSpan;
uniform float uSoftness;
uniform float uBend;
uniform float uFacet;
uniform float uBloom;
uniform vec3 uInk;
uniform vec3 uPearl;
uniform vec3 uWarm;
uniform vec3 uCool;

float saturate(float value) {
  return clamp(value, 0.0, 1.0);
}

float triangleWave(float value) {
  return abs(fract(value) * 2.0 - 1.0);
}

float softRidge(float distance, float width) {
  return exp(-distance * distance / max(width, 0.0001));
}

void main() {
  vec2 uv = vUv;
  vec2 pixel = 1.0 / max(uResolution, vec2(1.0));

  float mainAxis = mix(uv.x, uv.y, uAxis);
  float crossAxis = mix(uv.y, uv.x, uAxis);
  mainAxis = mix(mainAxis, 1.0 - mainAxis, step(0.0, -uForward));

  float time = uClock * 0.42;
  float drift =
      sin(crossAxis * 7.0 + time * 1.8) * 0.024
    + sin(crossAxis * 17.0 - time * 1.1 + 1.6) * 0.010
    + sin((mainAxis + crossAxis) * 11.0 + time * 0.7) * 0.006;
  drift *= uBend;

  float travel = -0.34 + uTravel * 1.68;
  float d = mainAxis - travel - drift;
  float ad = abs(d);

  float field = 1.0 - smoothstep(uSpan, uSpan + uSoftness, ad);
  float inner = 1.0 - smoothstep(uSpan * 0.28, uSpan, ad);
  float frontRidge = softRidge(d - uSpan * 0.34, 0.0017 + pixel.x * 16.0);
  float backRidge = softRidge(d + uSpan * 0.54, 0.0035 + pixel.y * 14.0) * 0.55;

  float facetGrid = triangleWave(crossAxis * uFacet + mainAxis * 2.1 + time * 0.35);
  float facetCuts = smoothstep(0.18, 0.92, facetGrid);
  float diagonal = triangleWave((crossAxis - mainAxis * 0.36) * (uFacet * 0.44) - time * 0.2);
  float caustic = pow(1.0 - diagonal, 4.0) * 0.42 + pow(facetCuts, 2.2) * 0.22;

  float glass = saturate(field * (0.5 + inner * 0.42) + frontRidge * 0.52 + backRidge * 0.2);
  float gateIn = smoothstep(0.02, 0.14, uTravel);
  float gateOut = 1.0 - smoothstep(0.9, 1.0, uTravel);
  float envelope = gateIn * gateOut;

  vec3 base = mix(uCool, uWarm, saturate(crossAxis * 0.82 + drift * 4.5));
  vec3 prism = mix(base, uPearl, inner * 0.36 + frontRidge * 0.5);
  prism = mix(prism, uInk, smoothstep(0.55, 0.0, ad) * 0.1);
  prism += caustic * uBloom * mix(uWarm, uPearl, 0.38);
  prism += frontRidge * uBloom * mix(uWarm, uCool, 0.22) * 0.72;
  prism += backRidge * uBloom * uCool * 0.7;

  float vignette = smoothstep(0.0, 0.025, crossAxis) * smoothstep(1.0, 0.975, crossAxis);
  float alpha = saturate(glass * 0.64 + frontRidge * 0.24 + backRidge * 0.14);
  alpha *= uOpacity * envelope * vignette;

  gl_FragColor = vec4(prism * alpha, alpha);
}
`;

const FULLSCREEN_TRIANGLES = new Float32Array([
  -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,
]);

const TONE_CONFIGS = {
  aperture: {
    bend: 0.42,
    bloom: 0.86,
    cool: [0.92, 0.78, 0.66],
    facet: 9.5,
    ink: [0.24, 0.16, 0.12],
    pearl: [1, 0.96, 0.88],
    softness: 0.076,
    span: 0.17,
    warm: [1, 0.62, 0.38],
  },
  beam: {
    bend: 0.12,
    bloom: 1.08,
    cool: [0.86, 0.72, 0.5],
    facet: 18,
    ink: [0.2, 0.13, 0.07],
    pearl: [1, 0.9, 0.68],
    softness: 0.038,
    span: 0.092,
    warm: [1, 0.48, 0.18],
  },
  chroma: {
    bend: 0.9,
    bloom: 0.78,
    cool: [0.38, 0.86, 0.84],
    facet: 13.5,
    ink: [0.18, 0.16, 0.3],
    pearl: [0.96, 0.88, 1],
    softness: 0.082,
    span: 0.15,
    warm: [1, 0.34, 0.72],
  },
  prism: {
    bend: 0.76,
    bloom: 0.58,
    cool: [0.28, 0.78, 0.86],
    facet: 12,
    ink: [0.2, 0.16, 0.26],
    pearl: [0.94, 0.82, 0.9],
    softness: 0.07,
    span: 0.14,
    warm: [0.98, 0.28, 0.58],
  },
} as const satisfies Record<
  PrismSweepTone,
  {
    bend: number;
    bloom: number;
    cool: readonly [number, number, number];
    facet: number;
    ink: readonly [number, number, number];
    pearl: readonly [number, number, number];
    softness: number;
    span: number;
    warm: readonly [number, number, number];
  }
>;

interface UniformMap {
  uAxis: WebGLUniformLocation | null;
  uBend: WebGLUniformLocation | null;
  uBloom: WebGLUniformLocation | null;
  uClock: WebGLUniformLocation | null;
  uCool: WebGLUniformLocation | null;
  uFacet: WebGLUniformLocation | null;
  uForward: WebGLUniformLocation | null;
  uInk: WebGLUniformLocation | null;
  uOpacity: WebGLUniformLocation | null;
  uPearl: WebGLUniformLocation | null;
  uResolution: WebGLUniformLocation | null;
  uSoftness: WebGLUniformLocation | null;
  uSpan: WebGLUniformLocation | null;
  uTravel: WebGLUniformLocation | null;
  uWarm: WebGLUniformLocation | null;
}

interface ShaderController {
  destroy: () => void;
  getTravel: () => number;
  setDirection: (direction: PrismSweepDirection) => void;
  setOpacity: (opacity: number) => void;
  setTone: (tone: PrismSweepTone) => void;
  setTravel: (travel: number) => void;
}

interface ShaderControllerState {
  animationFrame: number;
  buffer: WebGLBuffer | null;
  direction: PrismSweepDirection;
  gl: WebGLRenderingContext;
  opacity: number;
  program: WebGLProgram;
  startedAt: number;
  tone: PrismSweepTone;
  travel: number;
  uniforms: UniformMap;
}

interface SweepPlayback {
  cancel: () => void;
}

const clamp01 = (value: number) => Math.max(0, Math.min(1, value));

const directionToShader = (direction: PrismSweepDirection) => {
  switch (direction) {
    case "right":
      return { axis: 0, forward: -1 };
    case "up":
      return { axis: 1, forward: 1 };
    case "down":
      return { axis: 1, forward: -1 };
    default:
      return { axis: 0, forward: 1 };
  }
};

const easePrismTravel = (progress: number) => {
  const clamped = clamp01(progress);
  return 1 - (1 - clamped) ** 3.35;
};

const easeOpacityOut = (progress: number) => {
  const clamped = clamp01(progress);
  return clamped * clamped * (3 - 2 * clamped);
};

const compileShader = (
  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 createProgram = (gl: WebGLRenderingContext) => {
  const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
  const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);

  if (!(vertex && fragment)) {
    return null;
  }

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

  return program;
};

const resizeCanvas = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {
  const rect = canvas.getBoundingClientRect();
  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DEVICE_PIXEL_RATIO);
  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 getUniforms = (gl: WebGLRenderingContext, program: WebGLProgram) => ({
  uAxis: gl.getUniformLocation(program, "uAxis"),
  uBend: gl.getUniformLocation(program, "uBend"),
  uBloom: gl.getUniformLocation(program, "uBloom"),
  uClock: gl.getUniformLocation(program, "uClock"),
  uCool: gl.getUniformLocation(program, "uCool"),
  uFacet: gl.getUniformLocation(program, "uFacet"),
  uForward: gl.getUniformLocation(program, "uForward"),
  uInk: gl.getUniformLocation(program, "uInk"),
  uOpacity: gl.getUniformLocation(program, "uOpacity"),
  uPearl: gl.getUniformLocation(program, "uPearl"),
  uResolution: gl.getUniformLocation(program, "uResolution"),
  uSoftness: gl.getUniformLocation(program, "uSoftness"),
  uSpan: gl.getUniformLocation(program, "uSpan"),
  uTravel: gl.getUniformLocation(program, "uTravel"),
  uWarm: gl.getUniformLocation(program, "uWarm"),
});

const setRgbUniform = (
  gl: WebGLRenderingContext,
  location: WebGLUniformLocation | null,
  value: readonly [number, number, number]
) => {
  gl.uniform3f(location, value[0], value[1], value[2]);
};

const drawShader = (
  canvas: HTMLCanvasElement,
  state: ShaderControllerState
) => {
  const { gl, uniforms } = state;
  const directionUniforms = directionToShader(state.direction);
  const toneConfig = TONE_CONFIGS[state.tone];

  resizeCanvas(canvas, gl);
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.uniform2f(uniforms.uResolution, canvas.width, canvas.height);
  gl.uniform1f(uniforms.uClock, (performance.now() - state.startedAt) / 1000);
  gl.uniform1f(uniforms.uTravel, state.travel);
  gl.uniform1f(uniforms.uOpacity, state.opacity);
  gl.uniform1f(uniforms.uAxis, directionUniforms.axis);
  gl.uniform1f(uniforms.uForward, directionUniforms.forward);
  gl.uniform1f(uniforms.uSpan, toneConfig.span);
  gl.uniform1f(uniforms.uSoftness, toneConfig.softness);
  gl.uniform1f(uniforms.uBend, toneConfig.bend);
  gl.uniform1f(uniforms.uFacet, toneConfig.facet);
  gl.uniform1f(uniforms.uBloom, toneConfig.bloom);
  setRgbUniform(gl, uniforms.uInk, toneConfig.ink);
  setRgbUniform(gl, uniforms.uPearl, toneConfig.pearl);
  setRgbUniform(gl, uniforms.uWarm, toneConfig.warm);
  setRgbUniform(gl, uniforms.uCool, toneConfig.cool);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
};

const createShaderController = (
  canvas: HTMLCanvasElement,
  direction: PrismSweepDirection,
  tone: PrismSweepTone
) => {
  const gl = canvas.getContext("webgl", {
    alpha: true,
    antialias: false,
    depth: false,
    premultipliedAlpha: true,
    preserveDrawingBuffer: false,
    stencil: false,
  });

  if (!gl) {
    return null;
  }

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

  const buffer = gl.createBuffer();
  const position = gl.getAttribLocation(program, "position");

  // biome-ignore lint/correctness/useHookAtTopLevel: WebGLRenderingContext.useProgram is not a React hook.
  gl.useProgram(program);
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, FULLSCREEN_TRIANGLES, gl.STATIC_DRAW);
  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 state: ShaderControllerState = {
    animationFrame: 0,
    buffer,
    direction,
    gl,
    opacity: HIDDEN_OPACITY,
    program,
    startedAt: performance.now(),
    tone,
    travel: 0,
    uniforms: getUniforms(gl, program),
  };

  let destroyed = false;
  const tick = () => {
    if (destroyed) {
      return;
    }

    drawShader(canvas, state);
    state.animationFrame = requestAnimationFrame(tick);
  };

  state.animationFrame = requestAnimationFrame(tick);

  return {
    destroy: () => {
      destroyed = true;
      cancelAnimationFrame(state.animationFrame);
      state.gl.clear(state.gl.COLOR_BUFFER_BIT);
      if (state.buffer) {
        state.gl.deleteBuffer(state.buffer);
      }
      state.gl.deleteProgram(state.program);
    },
    getTravel: () => state.travel,
    setDirection: (nextDirection: PrismSweepDirection) => {
      state.direction = nextDirection;
    },
    setOpacity: (opacity: number) => {
      state.opacity = clamp01(opacity);
    },
    setTone: (nextTone: PrismSweepTone) => {
      state.tone = nextTone;
    },
    setTravel: (travel: number) => {
      state.travel = clamp01(travel);
    },
  } satisfies ShaderController;
};

const useShaderController = ({
  canvasRef,
  direction,
  tone,
}: {
  canvasRef: RefObject<HTMLCanvasElement | null>;
  direction: PrismSweepDirection;
  tone: PrismSweepTone;
}) => {
  const controllerRef = useRef<ShaderController | null>(null);

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

    const controller = createShaderController(canvas, direction, tone);
    controllerRef.current = controller;

    return () => {
      controller?.destroy();
      controllerRef.current = null;
    };
  }, [canvasRef, direction, tone]);

  useEffect(() => {
    controllerRef.current?.setDirection(direction);
    controllerRef.current?.setTone(tone);
  }, [direction, tone]);

  return controllerRef;
};

const playFrameSweep = (
  controller: ShaderController,
  {
    direction,
    duration,
    midpoint,
    onComplete,
    onMidpoint,
    outroMs,
  }: {
    direction: PrismSweepDirection;
    duration: number;
    midpoint: number;
    onComplete: () => void;
    onMidpoint: () => void;
    outroMs: number;
  }
): SweepPlayback => {
  let animationFrame = 0;
  let midpointReached = false;
  let cancelled = false;
  const startedAt = performance.now();
  const startTravel = controller.getTravel();
  const sweepMs = Math.max(MIN_PHASE_MS, duration * (1 - startTravel));

  controller.setDirection(direction);
  controller.setOpacity(VISIBLE_OPACITY);

  const finishSweep = () => {
    const outroStartedAt = performance.now();

    const fadeOut = () => {
      if (cancelled) {
        return;
      }

      const outroRaw = clamp01((performance.now() - outroStartedAt) / outroMs);
      controller.setOpacity(VISIBLE_OPACITY * (1 - easeOpacityOut(outroRaw)));

      if (outroRaw < 1) {
        animationFrame = requestAnimationFrame(fadeOut);
        return;
      }

      controller.setOpacity(HIDDEN_OPACITY);
      controller.setTravel(0);
      onComplete();
    };

    animationFrame = requestAnimationFrame(fadeOut);
  };

  const advance = () => {
    if (cancelled) {
      return;
    }

    const raw = clamp01((performance.now() - startedAt) / sweepMs);
    const nextTravel = startTravel + (1 - startTravel) * easePrismTravel(raw);
    controller.setTravel(nextTravel);

    if (!(midpointReached || nextTravel < midpoint)) {
      midpointReached = true;
      onMidpoint();
    }

    if (raw < 1) {
      animationFrame = requestAnimationFrame(advance);
      return;
    }

    if (!midpointReached) {
      onMidpoint();
    }
    controller.setTravel(1);
    finishSweep();
  };

  animationFrame = requestAnimationFrame(advance);

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

/**
 * PrismSweepTransition — a frame-level route/state transition with a custom
 * faceted caustic shader tuned for SmoothUI previews.
 */
const PrismSweepTransition = ({
  children,
  className,
  direction = "left",
  duration = DEFAULT_DURATION_MS,
  onRest,
  tone = "prism",
  transitionKey,
}: PrismSweepTransitionProps) => {
  const shouldReduceMotion = useReducedMotion();
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const playbackRef = useRef<SweepPlayback | null>(null);
  const [renderedChildren, setRenderedChildren] = useState(children);
  const [isSweeping, setIsSweeping] = useState(false);
  const previousKey = useRef(transitionKey);
  const controllerRef = useShaderController({ canvasRef, direction, tone });

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

    previousKey.current = transitionKey;
    playbackRef.current?.cancel();

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

    const controller = controllerRef.current;
    if (!controller) {
      setRenderedChildren(children);
      setIsSweeping(false);
      onRest?.();
      return;
    }

    setIsSweeping(true);
    playbackRef.current = playFrameSweep(controller, {
      direction,
      duration: Math.max(MIN_PHASE_MS, duration),
      midpoint: CONTENT_SWAP_RATIO,
      onComplete: () => {
        setIsSweeping(false);
        playbackRef.current = null;
        onRest?.();
      },
      onMidpoint: () => setRenderedChildren(children),
      outroMs: OUTRO_MS,
    });

    return () => {
      playbackRef.current?.cancel();
    };
  }, [
    children,
    controllerRef,
    direction,
    duration,
    onRest,
    shouldReduceMotion,
    transitionKey,
  ]);

  return (
    <div
      className={cn(
        "relative isolate overflow-hidden rounded-2xl bg-background text-foreground",
        className
      )}
      data-sweeping={isSweeping ? "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",
          isSweeping && !shouldReduceMotion ? "opacity-100" : "opacity-0"
        )}
        ref={canvasRef}
      />
    </div>
  );
};

export default PrismSweepTransition;

Componentes parecidos