Unstable Metallic Paint
Unstable Metallic Paint built with Skia's shader
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated @shopify/react-native-skiaCopy 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
