Skip to content
pixelfor Expo
Ctrl K

Visual

Fluid Orb

Flowing color rendered with the original procedural shader

Try it on your phone

Fluid Orb

Scan to open Fluid Orb in Expo Go

Install Expo Go first, then scan this QR to run the native component.

GET EXPO GO ONGoogle Play

This demo uses Expo SDK 54. If your installed Expo Go supports a different SDK, get the compatible build. iOS needs a build supporting this SDK.

Expo code

Original web component

Rare UI

Our Expo component

React Native Web

Component source

Make it yours

One file to copy into your app.

fluid-orb.tsx

import React, { useEffect, useRef, useState } from "react"
import { AppState, Text, View } from "react-native"
import { GLView, type ExpoWebGLRenderingContext } from "expo-gl"
import { useReducedMotion } from "react-native-reanimated"
const VERT = `attribute vec2 a_pos; void main(){gl_Position=vec4(a_pos,0.0,1.0);}`
const FRAG = `
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
uniform vec2 u_resolution; uniform float u_time; uniform vec3 u_color;
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);vec2 u=f*f*(3.0-2.0*f);return mix(mix(hash(i),hash(i+vec2(1.0,0.0)),u.x),mix(hash(i+vec2(0.0,1.0)),hash(i+vec2(1.0,1.0)),u.x),u.y);}
float fbm(vec2 p){float v=0.0;float a=0.6;for(int i=0;i<3;i++){v+=a*noise(p);p*=2.0;a*=0.5;}return v;}
void main(){vec2 uv=gl_FragCoord.xy/u_resolution.xy;float t=u_time*0.22;vec2 drift=vec2(sin(t)+0.6*sin(t*1.7+1.3),cos(t*0.8)+0.6*cos(t*1.3+2.1));vec2 p=vec2(uv.x*1.8,uv.y)+drift*0.7;vec2 q=vec2(fbm(p+drift),fbm(p+vec2(3.2,1.5)-drift));float f=fbm(p+1.2*q);float g=clamp(1.0-uv.y,0.0,1.0);float anchor=smoothstep(0.0,0.3,uv.y);float shade=clamp(g+(f-0.5)*0.8*anchor,0.0,1.0);vec3 white=vec3(0.99,1.0,1.0);vec3 light=mix(white,u_color,0.5);vec3 col=mix(white,light,smoothstep(0.28,0.52,shade));col=mix(col,u_color,smoothstep(0.58,0.88,shade));float edge=smoothstep(0.5,0.49,distance(uv,vec2(0.5)));gl_FragColor=vec4(col*edge,edge);}
`
export interface FluidOrbProps {
  size?: number
  color?: string
  testID?: string
}
export function FluidOrb({
  size = 240,
  color = "#1A73F2",
  testID,
}: FluidOrbProps) {
  const reduced = useReducedMotion(),
    cleanup = useRef<() => void>(() => {}),
    [error, setError] = useState<string | null>(null)
  useEffect(() => () => cleanup.current(), [])
  function context(gl: ExpoWebGLRenderingContext) {
    cleanup.current()
    const shaders: WebGLShader[] = [],
      buffer = gl.createBuffer(),
      program = gl.createProgram()
    let frame = 0,
      disposed = false,
      active = AppState.currentState === "active",
      last = 0,
      start = 0
    const subscription = AppState.addEventListener("change", (next) => {
      active = next === "active"
    })
    cleanup.current = () => {
      disposed = true
      cancelAnimationFrame(frame)
      subscription.remove()
      gl.deleteBuffer(buffer)
      if (program) gl.deleteProgram(program)
      shaders.forEach((s) => gl.deleteShader(s))
    }
    try {
      if (!program || !buffer) throw new Error("GL allocation failed")
      for (const [type, source] of [
        [gl.VERTEX_SHADER, VERT],
        [gl.FRAGMENT_SHADER, FRAG],
      ] as const) {
        const shader = gl.createShader(type)
        if (!shader) throw new Error("Shader allocation failed")
        shaders.push(shader)
        gl.shaderSource(shader, source)
        gl.compileShader(shader)
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
          throw new Error(
            gl.getShaderInfoLog(shader) ?? "Shader compilation failed"
          )
        gl.attachShader(program, shader)
      }
      gl.linkProgram(program)
      if (!gl.getProgramParameter(program, gl.LINK_STATUS))
        throw new Error(
          gl.getProgramInfoLog(program) ?? "Shader linking failed"
        )
      gl.useProgram(program)
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
      gl.bufferData(
        gl.ARRAY_BUFFER,
        new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
        gl.STATIC_DRAW
      )
      const pos = gl.getAttribLocation(program, "a_pos")
      gl.enableVertexAttribArray(pos)
      gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0)
      const resolution = gl.getUniformLocation(program, "u_resolution"),
        time = gl.getUniformLocation(program, "u_time"),
        tint = gl.getUniformLocation(program, "u_color")
      let hex = color.replace("#", "")
      if (hex.length === 3) hex = [...hex].map((x) => x + x).join("")
      const n = parseInt(hex, 16)
      gl.uniform3f(
        tint,
        ((n >> 16) & 255) / 255,
        ((n >> 8) & 255) / 255,
        (n & 255) / 255
      )
      function draw(now: number) {
        if (disposed) return
        if (!start) start = now
        if (active && (reduced || now - last >= 1000 / 30)) {
          last = now
          gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
          gl.uniform2f(
            resolution,
            gl.drawingBufferWidth,
            gl.drawingBufferHeight
          )
          gl.uniform1f(time, reduced ? 0 : (now - start) / 1000)
          gl.clearColor(0, 0, 0, 0)
          gl.clear(gl.COLOR_BUFFER_BIT)
          gl.drawArrays(gl.TRIANGLES, 0, 6)
          gl.flush()
          gl.endFrameEXP()
        }
        if (!reduced) frame = requestAnimationFrame(draw)
      }
      frame = requestAnimationFrame(draw)
    } catch (e) {
      cleanup.current()
      setError(String(e))
    }
  }
  return (
    <View
      testID={testID}
      accessible
      accessibilityLabel="Fluid color orb"
      style={{
        width: size,
        height: size,
        alignSelf: "center",
        borderRadius: size / 2,
        overflow: "hidden",
      }}
    >
      {error ? (
        <Text style={{ color: "#aaa", padding: 16 }} accessibilityRole="alert">
          GL preview unavailable
        </Text>
      ) : (
        <GLView
          key={`${color}-${reduced}`}
          style={{ flex: 1 }}
          onContextCreate={context}
        />
      )}
    </View>
  )
}
export default FluidOrb

Give the gestures and motion a run on a real Android phone.

Back to Rare UI