Skip to content

Instantly share code, notes, and snippets.

@LeslieOA
Forked from Saadnajmi/Ripple.tsx
Created December 30, 2024 12:06
Show Gist options
  • Save LeslieOA/56f1b383539aa70cf605436a6ace8940 to your computer and use it in GitHub Desktop.
Save LeslieOA/56f1b383539aa70cf605436a6ace8940 to your computer and use it in GitHub Desktop.
Ripple Effect Shader with react-native-skia
import { Canvas, Circle, Group, Rect, Shader, Skia, useClock, RuntimeShader, SkRuntimeEffect, RoundedRect } from "@shopify/react-native-skia";
import { SafeAreaView, StyleSheet } from "react-native";
import { useDerivedValue, useSharedValue } from "react-native-reanimated";
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
const styles = StyleSheet.create({
centered: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
const rippleShader = Skia.RuntimeEffect.Make(`
uniform float2 u_origin;
uniform float u_time;
uniform float u_amplitude;
uniform float u_frequency;
uniform float u_decay;
uniform float u_speed;
uniform shader image;
half4 main(float2 position) {
// The distance of the current pixel position from origin
float dist = distance(position, u_origin);
// The amount of time it takes for the ripple to arrive at the current pixel position.
float delay = dist / u_speed;
// Adjust for delay, clamp to 0.
float time = u_time - delay;
time -= delay;
time = max(0.0, time);
// The ripple is a sine wave that scales by an exponential decay function.
float rippleAmount = u_amplitude * sin(u_frequency * time) * exp(-u_decay * time);
// A vector of length amplitude that points away from position.
float2 n = normalize(position - u_origin);
// Scale n by the ripple amount at the current pixel position and add it
// to the current pixel position.
//
// This new position moves toward or away from origin based on the
// sign and magnitude of rippleAmount.
float2 newPosition = position + rippleAmount * n;
// Sample the layer at the new position.
half4 color = image.eval(newPosition).rgba;
// Lighten or darken the color based on the ripple amount and its alpha component.
color.rgb += 0.3 * (rippleAmount / u_amplitude) * color.a;
return color;
}
`) as SkRuntimeEffect;
export const Ripple = () => {
const width = 300;
const height = 600;
const duration = 2;
const clock = useClock();
const uniforms = useDerivedValue(() => ({ iTime: clock.value / 1000 }), [clock]);
const touchX = useSharedValue(0);
const touchY = useSharedValue(0);
const touchStart = useSharedValue(0);
const touchPoint = useDerivedValue(() => (
{ x: touchX.value, y: touchY.value }
), [touchX, touchY]);
const elapsedTime = useDerivedValue(() => (
(clock.value - touchStart.value) / 1000
), [clock, touchStart]);
const shaderEnabled = useDerivedValue(() => {
return rippleShader && 0 < elapsedTime.value && (elapsedTime.value < duration);
}, [rippleShader, elapsedTime]);
const rippleUniforms = useDerivedValue(() => ({
u_origin: touchPoint.value,
u_time: elapsedTime.value,
u_amplitude: 12,
u_frequency: 15,
u_decay: 10,
u_speed: 1200,
}), [clock, touchStart, touchPoint]);
const tap = Gesture.Tap().onBegin((e) => {
touchX.value = e.x;
touchY.value = e.y;
touchStart.value = clock.value;
console.log('tap', e.x, e.y, 'time', clock.value);
});
return (
<SafeAreaView style={styles.centered}>
<GestureHandlerRootView>
<GestureDetector gesture={tap}>
<Canvas style={{width: width, height: height}}>
{shaderEnabled && <RuntimeShader source={rippleShader} uniforms={rippleUniforms} />}
<RoundedRect x={10} y={10} r={20} width={250} height={500} color={'red'} />
</Canvas>
</GestureDetector>
</GestureHandlerRootView>
</SafeAreaView>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment