Wave Scrawler

Wave Scrawler 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 react-native-worklets

Copy and paste the following code into your project. component/organisms/wave-scrawler

// @ts-checkimport { StyleSheet, View, type LayoutChangeEvent } from "react-native";import React, { useEffect, useMemo, memo, useState } from "react";import {  Canvas,  Skia,  Fill,  Shader,  ImageShader,  Group,  rect,  type SkRuntimeEffect,  type Uniforms,} from "@shopify/react-native-skia";import {  useSharedValue,  withTiming,  useDerivedValue,  Easing,} from "react-native-reanimated";import type { IWaveScrawler, ITransitionRenderer } from "./types";import { WAVE_SCRAWLER_SHADER, DEFAULT_CONFIG } from "./conf";import { scheduleOnRN } from "react-native-worklets";import { useLoadedImages } from "./hooks";const TransitionRenderer: React.FC<ITransitionRenderer> &  React.FunctionComponent<ITransitionRenderer> = memo<ITransitionRenderer>(  ({    fromImage,    toImage,    progress,    width,    height,    amplitude,    waves,    colorSeparation,  }: ITransitionRenderer):    | (React.JSX.Element & React.ReactNode & React.ReactElement)    | null => {    const runtimeEffect = useMemo<SkRuntimeEffect | null>(() => {      return Skia.RuntimeEffect.Make(WAVE_SCRAWLER_SHADER);    }, []);    const uniforms = useDerivedValue<Uniforms>(() => {      return {        progress: progress.value,        resolution: [width, height],        amplitude,        waves,        colorSeparation,      };    }, [progress, width, height, amplitude, waves, colorSeparation]);    if (      !runtimeEffect ||      !fromImage ||      !toImage ||      width === 0 ||      height === 0    ) {      if (fromImage && width > 0 && height > 0) {        return (          <Group>            <ImageShader              image={fromImage}              fit="cover"              rect={rect(0, 0, width, height)}            />            <Fill />          </Group>        );      }      return null;    }    return (      <Fill>        <Shader source={runtimeEffect} uniforms={uniforms}>          <ImageShader            image={fromImage}            fit="cover"            x={0}            y={0}            width={width}            height={height}          />          <ImageShader            image={toImage}            fit="cover"            x={0}            y={0}            width={width}            height={height}          />        </Shader>      </Fill>    );  },);export const WaveScrawler: React.FC<IWaveScrawler> &  React.FunctionComponent<IWaveScrawler> = memo<IWaveScrawler>(  ({    source,    index,    duration = DEFAULT_CONFIG.duration,    amplitude = DEFAULT_CONFIG.amplitude,    waves = DEFAULT_CONFIG.waves,    colorSeparation = DEFAULT_CONFIG.colorSeparation,    style,    onTransitionEnd,  }: IWaveScrawler):    | (React.ReactNode & React.JSX.Element & React.ReactElement)    | null => {    const [dimensions, setDimensions] = useState<{      width: number;      height: number;    }>({ width: 0, height: 0 });    const [fromIndex, setFromIndex] = useState<number>(index);    const [toIndex, setToIndex] = useState<number>(index);    const progress = useSharedValue<number>(1);    const isAnimating = useSharedValue<boolean>(false);    const loadedImages = useLoadedImages(source);    const onLayout = <T extends LayoutChangeEvent>(event: T) => {      const { width, height } = event.nativeEvent.layout;      setDimensions({ width, height });    };    const handleTransitionEnd = <T extends number>(newIndex: T) => {      onTransitionEnd?.(newIndex);    };    useEffect(() => {      if (index !== toIndex && !isAnimating.value) {        setFromIndex(toIndex);        setToIndex(index);        progress.value = 0;        isAnimating.value = true;        progress.value = withTiming<number>(          1,          {            duration,            easing: Easing.bezier(0.4, 0, 0.2, 1),          },          (finished) => {            if (finished) {              isAnimating.value = false;              scheduleOnRN<[number], void>(handleTransitionEnd, index);            }          },        );      }    }, [index]);    const fromImage = loadedImages[fromIndex] ?? null;    const toImage = loadedImages[toIndex] ?? null;    return (      <View style={[styles.container, style]} onLayout={onLayout}>        <Canvas style={styles.canvas}>          <TransitionRenderer            fromImage={fromImage}            toImage={toImage}            progress={progress}            width={dimensions.width}            height={dimensions.height}            amplitude={amplitude}            waves={waves}            colorSeparation={colorSeparation}          />        </Canvas>      </View>    );  },);const styles = StyleSheet.create({  container: {    overflow: "hidden",  },  canvas: {    flex: 1,  },});export default memo<  React.FC<IWaveScrawler> & React.FunctionComponent<IWaveScrawler>>(WaveScrawler);export type { IWaveScrawler, ImageSource, ITransitionRenderer } from "./types";

Usage

import React, { useState } from "react";import { StyleSheet, View, Pressable } from "react-native";import WaveScrawler from "@/components/organisms/wave-scrawler";const IMAGES = [  {    uri: "https://i.pinimg.com/736x/9d/78/88/9d7888052044c88954e1af4c7c9699e6.jpg",  },  {    uri: "https://i.pinimg.com/1200x/ca/ce/8f/cace8f011661cac3b001b6c004598353.jpg",  },  {    uri: "https://i.pinimg.com/1200x/18/78/37/18783753f9ec9084a25a731580357b84.jpg",  },];export default function WaveScrawlerExample() {  const [index, setIndex] = useState(0);  const next = () => setIndex((i) => (i + 1) % IMAGES.length);  return (    <View style={styles.container}>      <View style={styles.card}>        <WaveScrawler          source={IMAGES}          index={index}          duration={1000}          // amplitude={0.108}          // colorSeparation={0.095}          style={styles.scrawler}        />        <Pressable onPress={next} style={StyleSheet.absoluteFill} />      </View>    </View>  );}const styles = StyleSheet.create({  container: {    flex: 1,    backgroundColor: "#111",    justifyContent: "center",    alignItems: "center",  },  card: {    width: 320,    height: 420,    borderRadius: 24,    overflow: "hidden",    bottom: 150,    backgroundColor: "#1a1a1a",  },  scrawler: {    flex: 1,  },  dots: {    position: "absolute",    bottom: 16,    alignSelf: "center",    flexDirection: "row",    gap: 6,  },  dot: {    width: 6,    height: 6,    borderRadius: 3,    backgroundColor: "rgba(255,255,255,0.4)",  },  dotActive: {    backgroundColor: "#fff",  },});

Props

React Native Reanimated
React Native Skia
React Native Worklets