Veltrix

skia shaders

Signal Orb

A restrained AI presence orb that morphs continuously across assistant states and responds to live audio levels.

AppleiOSAndroidAndroidExpoExpo Go
skiareanimated
GitHubOpen in GitHub

Installation

Install the dependencies, then copy the files for your preferred renderer.

pnpm add @shopify/react-native-skia react-native-reanimated

Behavior

  • state selects the motion pattern and color sets its base color.
  • levels drives the listening and speaking states.
  • Reduced Motion preserves state changes without continuous movement.

Install tailwind variants

pnpm add tailwind-variants

Usage

AssistantPresence.tsx
import { SignalOrb } from "@animations/ui/components/skia-shaders/signal-orb/uniwind";export function AssistantPresence() {  return (    <SignalOrb      className="self-center"      size={180}      state="thinking"    />  );}

Component files

6 files

signal-orb/config.ts
import {  SIGNAL_ORB_STATES,  type SignalOrbState,} from "./types";export const SIGNAL_ORB_CONFIG = {  color: "#45cdb8",  levelDuration: 220,  sampleCount: 16,  size: 160,  transitionDuration: 900,} as const;export const SIGNAL_ORB_STATE_INDEX = Object.fromEntries(  SIGNAL_ORB_STATES.map((state, index) => [state, index]),) as Record<SignalOrbState, number>;
signal-orb/signal-orb-layer.tsx
import {  Canvas,  Fill,  Shader,  Skia,  useCanvasSize,  useClock,} from "@shopify/react-native-skia";import { StyleSheet } from "react-native";import {  useDerivedValue,  useReducedMotion,} from "react-native-reanimated";import {  SIGNAL_ORB_CONFIG,  SIGNAL_ORB_STATE_INDEX,} from "./config";import type { SignalOrbBaseProps } from "./types";import { useSignalOrb } from "./use-signal-orb";const signalOrbEffect = (() => {  const effect = Skia.RuntimeEffect.Make(`  uniform float2 resolution;  uniform float time;  uniform float weights[6];  uniform float energy;  uniform float peak;  uniform float variation;  uniform float motion;  uniform float4 tint;  float ring(float distanceToRing, float width, float antialias) {    return 1.0 - smoothstep(      width - antialias,      width + antialias,      abs(distanceToRing)    );  }  float stateWave(    float angle,    float frequency,    float offset,    float phase,    float rotationWeight  ) {    float stationary = sin(angle * frequency + offset);    float rotating = sin(angle * frequency + offset + phase);    return mix(stationary, rotating, rotationWeight);  }  half4 main(float2 position) {    float minimumSize = max(1.0, min(resolution.x, resolution.y));    float2 point = (position - resolution * 0.5) / minimumSize;    float angle = atan(point.y, point.x);    float idle = weights[${SIGNAL_ORB_STATE_INDEX.idle}];    float connecting = weights[${SIGNAL_ORB_STATE_INDEX.connecting}];    float listening = weights[${SIGNAL_ORB_STATE_INDEX.listening}];    float thinking = weights[${SIGNAL_ORB_STATE_INDEX.thinking}];    float speaking = weights[${SIGNAL_ORB_STATE_INDEX.speaking}];    float error = weights[${SIGNAL_ORB_STATE_INDEX.error}];    float phase = time * 2.4 * motion;    float rotationWeight = clamp(      idle + connecting + thinking + error,      0.0,      1.0    );    float audioWeight = listening + speaking;    float audioResponse = energy * audioWeight;    float connectingPulse = connecting *      (0.5 + 0.5 * sin(phase * 2.4));    float listeningBreath = listening * energy *      (0.5 + 0.5 * sin(phase * 1.2));    float speakingEnergy = speaking * (0.35 + energy * 0.65);    float speakingPulseA = speakingEnergy *      (0.5 + 0.5 * sin(phase * 1.6));    float speakingPulseB = speakingEnergy *      (0.5 + 0.5 * sin(phase * 1.6 + 1.9));    float speakingPulseC = speakingEnergy *      (0.5 + 0.5 * sin(phase * 1.6 + 3.8));    float listeningFold = listening * energy *      (0.060 + 0.025 * (0.5 + 0.5 * sin(phase * 1.2))) *      (0.5 + 0.5 * cos(angle * 3.0 + 0.4));    float stateMotion =      idle * 0.005 +      connecting * 0.014 +      listening * 0.012 +      thinking * 0.018 +      speaking * 0.014 +      error * 0.016;    float audioDeformation = energy * (listening * 0.035 + speaking * 0.050) +      variation * audioWeight * 0.025;    float deformation = stateMotion + audioDeformation;    float drift =      idle * 0.003 +      connecting * 0.012 +      listening * 0.008 +      thinking * 0.016 +      speaking * 0.012 +      error * 0.012 +      audioResponse * 0.006;    float ringSpread =      idle * 0.014 +      connecting * (0.018 + connectingPulse * 0.008) +      listening * 0.016 +      thinking * 0.023 +      speaking * 0.019 +      error * 0.018;    float baseRadius = 0.385 + connectingPulse * 0.008 +      listeningBreath * 0.012 + audioResponse *      (listening * 0.018 + speaking * 0.018);    float rotatingDrift = drift * rotationWeight;    float2 pointA = point - float2(cos(phase * 0.82), sin(phase * 0.68)) * rotatingDrift;    float2 pointB = point - float2(cos(phase * 0.61 + 2.1), sin(phase * 0.91 + 1.4)) * rotatingDrift;    float2 pointC = point - float2(cos(phase * 0.74 + 4.0), sin(phase * 0.57 + 3.2)) * rotatingDrift;    float thinkingRipple = thinking * (      sin(angle * 5.0 - phase * 1.4) * 0.018 +      sin(angle * 3.0 + phase * 1.9) * 0.010    );    float errorTension = error * sin(angle * 6.0 + phase * 1.8) * 0.008;    float radiusA = baseRadius - ringSpread +      stateWave(angle, 2.0, 0.0, phase * 1.15, rotationWeight) * deformation +      stateWave(angle, 3.0, 0.0, -phase * 0.72, rotationWeight) * deformation * 0.38 +      thinkingRipple + errorTension +      listeningBreath * 0.006 + speakingPulseA * 0.012 - listeningFold;    float radiusB = baseRadius +      stateWave(angle, 2.0, 2.0, -phase * 0.94, rotationWeight) * deformation * 0.86 +      stateWave(angle, 4.0, 0.0, phase * 0.58, rotationWeight) * deformation * 0.30 -      thinkingRipple * 0.65 +      listeningBreath * 0.006 + speakingPulseB * 0.012 - listeningFold;    float radiusC = baseRadius + ringSpread +      stateWave(angle, 3.0, 4.1, phase * 0.76, rotationWeight) * deformation * 0.72 +      stateWave(angle, 2.0, 0.0, -phase * 0.49, rotationWeight) * deformation * 0.34 -      errorTension * 0.7 +      listeningBreath * 0.006 + speakingPulseC * 0.012 - listeningFold;    float distanceA = length(pointA) - radiusA;    float distanceB = length(pointB) - radiusB;    float distanceC = length(pointC) - radiusC;    float antialias = 1.5 / minimumSize;    float strokeWidth = 0.011 + connectingPulse * 0.003 +      audioResponse * 0.006 + speakingEnergy * 0.006;    float ringA = ring(distanceA, strokeWidth, antialias);    float ringB = ring(distanceB, strokeWidth, antialias);    float ringC = ring(distanceC, strokeWidth, antialias);    float3 colorA = mix(      tint.rgb * 0.88,      float3(0.88, 0.34, 0.30),      error    );    float3 colorB = mix(      min(tint.rgb * 1.08 + float3(0.04), float3(1.0)),      float3(0.96, 0.48, 0.38),      error    );    float3 colorC = mix(colorA, colorB, 0.52);    float coverage = ringA + ringB + ringC;    float3 color = (      colorA * ringA +      colorB * ringB +      colorC * ringC    ) / max(coverage, 0.001);    float halo = (      exp(-abs(distanceA) * 58.0) +      exp(-abs(distanceB) * 58.0) +      exp(-abs(distanceC) * 58.0)    ) * (0.025 + peak * audioWeight * 0.018);    float alpha = clamp(coverage * 0.82 + halo, 0.0, 1.0);    return half4(      color.r * alpha,      color.g * alpha,      color.b * alpha,      alpha    );  }  `);  if (!effect) {    throw new Error("Signal Orb shader failed to compile.");  }  return effect;})();type SignalOrbLayerProps = Pick<  SignalOrbBaseProps,  "color" | "levels" | "state">;export function SignalOrbLayer({  color,  levels,  state,}: SignalOrbLayerProps) {  const {    ref,    size: { height, width },  } = useCanvasSize();  const clock = useClock();  const reducedMotion = useReducedMotion();  const tint = Skia.Color(    color ?? SIGNAL_ORB_CONFIG.color,  );  const { energy, peak, stateWeights, variation } =    useSignalOrb({ levels, state });  const uniforms = useDerivedValue(() => ({    energy: energy.get(),    motion: reducedMotion ? 0 : 1,    peak: peak.get(),    resolution: [width, height],    time: reducedMotion ? 0 : clock.get() / 1_000,    tint,    variation: variation.get(),    weights: stateWeights.get(),  }));  return (    <Canvas      pointerEvents="none"      ref={ref}      style={StyleSheet.absoluteFill}    >      <Fill>        <Shader source={signalOrbEffect} uniforms={uniforms} />      </Fill>    </Canvas>  );}
signal-orb/types.ts
export const SIGNAL_ORB_STATES = [  "idle",  "connecting",  "listening",  "thinking",  "speaking",  "error",] as const;export type SignalOrbState =  (typeof SIGNAL_ORB_STATES)[number];export type SignalOrbBaseProps = {  /** Accessible description of the assistant and its current state. */  accessibilityLabel?: string;  /** Base color used by every state except the error tint. */  color?: string;  /** Recent normalized audio samples used by listening and speaking states. */  levels?: readonly number[];  /** Height and width of the square drawing surface. */  size?: number;  /** Current assistant state. Changes morph the existing shader continuously. */  state: SignalOrbState;};
signal-orb/uniwind/index.tsx
import { View } from "react-native";import { SIGNAL_ORB_CONFIG } from "../config";import { SignalOrbLayer } from "../signal-orb-layer";import type { SignalOrbBaseProps } from "../types";import { signalOrbVariants } from "./variants";export type SignalOrbProps = SignalOrbBaseProps & {  className?: string;};/** Minimal shader orb for continuous AI assistant states. */export function SignalOrb({  accessibilityLabel,  className,  color,  levels,  size = SIGNAL_ORB_CONFIG.size,  state,}: SignalOrbProps) {  return (    <View      accessibilityLabel={        accessibilityLabel ?? `AI assistant ${state}`      }      accessibilityRole="image"      accessible      className={signalOrbVariants({ className })}      style={{ height: size, width: size }}    >      <SignalOrbLayer        color={color}        levels={levels}        state={state}      />    </View>  );}
signal-orb/uniwind/variants.ts
import { tv } from "tailwind-variants";export const signalOrbVariants = tv({  base: "relative items-center justify-center",});
signal-orb/use-signal-orb.ts
import { useEffect } from "react";import {  Easing,  useSharedValue,  withTiming,} from "react-native-reanimated";import { SIGNAL_ORB_CONFIG } from "./config";import {  SIGNAL_ORB_STATES,  type SignalOrbState,} from "./types";function getStateWeights(state: SignalOrbState) {  return SIGNAL_ORB_STATES.map((item) =>    item === state ? 1 : 0,  );}function getLevelMetrics(levels: readonly number[] | undefined) {  const samples = levels    ?.slice(-SIGNAL_ORB_CONFIG.sampleCount)    .map((level) => Math.min(1, Math.max(0, level))) ?? [];  if (samples.length === 0) {    return { energy: 0, peak: 0, variation: 0 };  }  let squaredTotal = 0;  let peak = 0;  let variationTotal = 0;  for (let index = 0; index < samples.length; index += 1) {    const sample = samples[index];    squaredTotal += sample * sample;    peak = Math.max(peak, sample);    if (index > 0) {      variationTotal += Math.abs(sample - samples[index - 1]);    }  }  return {    energy: Math.sqrt(squaredTotal / samples.length),    peak,    variation:      samples.length > 1        ? variationTotal / (samples.length - 1)        : 0,  };}export function useSignalOrb({  levels,  state,}: {  levels?: readonly number[];  state: SignalOrbState;}) {  const metrics = getLevelMetrics(levels);  const energy = useSharedValue(metrics.energy);  const peak = useSharedValue(metrics.peak);  const stateWeights = useSharedValue(    getStateWeights(state),  );  const variation = useSharedValue(metrics.variation);  useEffect(() => {    stateWeights.set(      withTiming(getStateWeights(state), {        duration: SIGNAL_ORB_CONFIG.transitionDuration,        easing: Easing.inOut(Easing.cubic),      }),    );  }, [state, stateWeights]);  useEffect(() => {    const timing = {      duration: SIGNAL_ORB_CONFIG.levelDuration,      easing: Easing.out(Easing.quad),    };    energy.set(withTiming(metrics.energy, timing));    peak.set(withTiming(metrics.peak, timing));    variation.set(withTiming(metrics.variation, timing));  }, [energy, metrics.energy, metrics.peak, metrics.variation, peak, variation]);  return {    energy,    peak,    stateWeights,    variation,  };}

API reference

Generated directly from the exported component props.

SignalOrb

Minimal shader orb for continuous AI assistant states.