Unstable Metallic Paint

Unstable Metallic Paint 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_metallic-paint

/** * Metallic Paint Shader Component * Inspired by ReactBits * https://reactbits.dev/animations/metallic-paint */import React, { memo, useMemo } from "react";import {  StyleSheet,  View,  useWindowDimensions,  type ViewStyle,} from "react-native";import {  Canvas,  Skia,  Shader,  Fill,  ImageShader,  useImage,  type SkRuntimeEffect as RuntimeEffect,} from "@shopify/react-native-skia";import {  useSharedValue,  useFrameCallback,  useDerivedValue,  type SharedValue,} from "react-native-reanimated";import type { MetallicPaintProps } from "./types";import { SHADER_SOURCE } from "./const";const UnStableMetallicPaintComponent: React.FC<MetallicPaintProps> = ({  unstable_uri: imageUri,  size = 200,  patternScale = 1.5,  refraction = 0.025,  edge = 1.5,  patternBlur = 0.003,  liquid = 0.12,  speed = 0.5,}) => {  const image = useImage(imageUri);  const time: SharedValue<number> = useSharedValue(0);  const { width: windowWidth, height: windowHeight } = useWindowDimensions();  const width = size ?? windowWidth;  const height = size ?? windowHeight;  useFrameCallback(({ timeSincePreviousFrame }) => {    if (timeSincePreviousFrame != null) {      time.value += timeSincePreviousFrame * speed;    }  });  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;  }, []);  const uniforms = useDerivedValue(    () => ({      u_time: time.value,      u_ratio: width / height,      u_patternScale: patternScale,      u_refraction: refraction,      u_edge: edge,      u_patternBlur: patternBlur,      u_liquid: liquid,      u_resolution: [width, height] as const,    }),    [width, height, patternScale, refraction, edge, patternBlur, liquid],  );  if (!shader || !image) return null;  const containerStyle: ViewStyle | undefined = size    ? { width: size, height: size }    : undefined;  return (    <View style={[styles.container, containerStyle]}>      <Canvas style={styles.canvas}>        <Fill>          <Shader source={shader} uniforms={uniforms}>            <ImageShader              image={image}              x={0}              y={0}              width={width}              height={height}              fit="contain"            />          </Shader>        </Fill>      </Canvas>    </View>  );};export const UnStableMetallicPaint = memo(UnStableMetallicPaintComponent);const styles = StyleSheet.create({  container: {},  canvas: {    flex: 1,  },});export default memo(UnStableMetallicPaint);

Usage

import React from "react";import { StyleSheet, Text, View, Image as NativeImage } from "react-native";import { StatusBar } from "expo-status-bar";import { useFonts } from "expo-font";import { UnStableMetallicPaint } from "@/components/organisms/unstable_metallic-paint";export default function HomeScreen() {  const [fontLoaded] = useFonts({    SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"),    HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"),  });  const logo = NativeImage.resolveAssetSource(    require("../assets/white_glow.png"),  ).uri;  return (    <View style={styles.container}>      <StatusBar style="light" />      <UnStableMetallicPaint        unstable_uri={logo}        size={500}        liquid={1.5}        patternBlur={0}      />    </View>  );}const styles = StyleSheet.create({  container: {    flex: 1,    backgroundColor: "#000",    alignItems: "center",  },  marqueeWrapper: {    alignSelf: "stretch",    marginHorizontal: 20,    paddingVertical: 12,    borderRadius: 20,    backgroundColor: "rgba(255,255,255,0.04)",    // optional – VERY subtle depth    shadowColor: "#000",    shadowOpacity: 0.15,    shadowRadius: 20,    shadowOffset: { width: 0, height: 8 },    overflow: "hidden",  },});

Props

React Native Reanimated
React Native Skia