Skip to content

Instantly share code, notes, and snippets.

@Saadnajmi
Created December 29, 2024 06:31
Show Gist options
  • Select an option

  • Save Saadnajmi/9d46dcd093068404239ee32402d93e41 to your computer and use it in GitHub Desktop.

Select an option

Save Saadnajmi/9d46dcd093068404239ee32402d93e41 to your computer and use it in GitHub Desktop.

Revisions

  1. Saadnajmi created this gist Dec 29, 2024.
    109 changes: 109 additions & 0 deletions Ripple.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,109 @@
    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>
    );
    };