Unstable Orb

Unstable Orb built with Skia's shader

Last updated on

Edit on GitHub

Manual

Install the following dependencies:

npm install react-native-reanimated @shopify/react-native-skia

Copy and paste the following code into your project. component/organisms/unstable_orb

import React, { memo, useMemo } from "react";import { StyleSheet } from "react-native";import {  Canvas,  Fill,  Skia,  Shader,  type SkRuntimeEffect as RuntimeEffect,  type Uniforms,} from "@shopify/react-native-skia";import {  useSharedValue,  useFrameCallback,  useDerivedValue,  withTiming,} from "react-native-reanimated";import { hexToRgb } from "./helper";import type { UnstableOrbProps } from "./types";import { SHADER_SOURCE } from "./conf";export const UnstableOrb: React.FC<UnstableOrbProps> &  React.FunctionComponent<UnstableOrbProps> = memo<UnstableOrbProps>(  ({    colorShift = 0,    intensity = 0.2,    background = "#000000",    colors = ["#9C43FE", "#4CC2E9", "#101499"],    style = { width: 200, height: 200 },  }: UnstableOrbProps): React.ReactNode & React.JSX.Element => {    const size = useSharedValue({ width: 0, height: 0 });    const time = useSharedValue<number>(0);    const animatedIntensity = useSharedValue<number>(intensity);    const shader: RuntimeEffect | null = useMemo(() => {      const effect = Skia.RuntimeEffect.Make(SHADER_SOURCE);      if (!effect) {        console.error("[MetallicPaint] Failed to create runtime effect");        return null;      }      return effect;    }, []);    useFrameCallback(() => {      time.value += 0.016;    });    React.useEffect(() => {      animatedIntensity.value = withTiming<number>(intensity, {        duration: 400,      });    }, [intensity]);    const [bgR, bgG, bgB] = hexToRgb<string>(background);    const [orb1R, orb1G, orb1B] = hexToRgb<string>(colors[0]);    const [orb2R, orb2G, orb2B] = hexToRgb<string>(colors[1]);    const [orb3R, orb3G, orb3B] = hexToRgb<string>(colors[2]);    const uniforms = useDerivedValue<Uniforms>(() => ({      iTime: time.value,      iResolution: [size.value.width, size.value.height],      colorShift: colorShift,      distortion: animatedIntensity.value,      bgColor: [bgR, bgG, bgB],      orbColor1: [orb1R, orb1G, orb1B],      orbColor2: [orb2R, orb2G, orb2B],      orbColor3: [orb3R, orb3G, orb3B],    }));    return (      <Canvas style={[styles.canvas, style]} onSize={size}>        <Fill>          <Shader source={shader as RuntimeEffect} uniforms={uniforms} />        </Fill>      </Canvas>    );  },);const styles = StyleSheet.create({  canvas: {    flex: 1,  },});export default memo<UnstableOrbProps>(UnstableOrb);

Usage

import React, { useState } from "react";import { Button, StyleSheet, View } from "react-native";import { StatusBar } from "expo-status-bar";import { UnstableOrb } from "@/components/organisms/unstable_orb";export default function HomeScreen() {  const [intensity, setIntensity] = useState<number>(0.6);  return (    <View style={styles.container}>      <StatusBar style="light" />      <View style={styles.orbWrapper}>        <UnstableOrb          intensity={intensity}          colorShift={0.15}          style={{ width: 220, height: 220 }}        />      </View>      <View style={{ position: "absolute", bottom: 400 }}>        <Button          title={intensity > 0 ? "Stop Thinking" : "Start Thinking"}          onPress={() => {            setIntensity(intensity > 0 ? 0 : 2);          }}        />      </View>    </View>  );}const styles = StyleSheet.create({  container: {    flex: 1,    backgroundColor: "#000",    justifyContent: "center",    alignItems: "center",  },  orbWrapper: {    borderRadius: 28,    padding: 16,    bottom: 300,    overflow: "hidden",    // barely-there depth (optional)    shadowColor: "#000",    shadowOpacity: 0.2,    shadowRadius: 24,    shadowOffset: { width: 0, height: 12 },  },});

Props

React Native Reanimated
React Native Skia