Spirals are simply magical.

Honestly, you'll not use them in your day-to-day projects, but they are still beautiful and fun to play with.

In this article, we'll create an animated logarithmic spiral by combining React Native Skia and Reanimated.
We'll start with the math behind these spirals, implement it in code, and use Skia to render smooth animations of our spiral design.

Hopefully, even though the animation won't be used frequently, you'll learn a lot of new interesting things.

Feel free to check out the source code on GitHub:

We'll start by creating a new Expo project.

npx create-expo-app animated-spiral --template expo-template-blank-typescript

Then, we'll install the necessary dependencies.

npx expo install react-native-reanimated @shopify/react-native-skia

That's it! We're ready to start coding.

First of all, we need to create a spiral. In this specific use case I was deeply inspired by the definition of logarithmic spiral.

$r = a \cdot \exp(k \cdot \theta)$

Where $r$ is the radius, $a$ is the constant, $k$ is the growth factor and $\theta$ is the angle.

In the implementation, I opted for the Cartesian coordinate system rather than the polar system. This choice necessitates a conversion from polar to Cartesian coordinates. The conversion is straightforward and allows for more intuitive manipulation of the spiral's position in the 2D plane. Here's how we transform the polar coordinates (r, θ) to Cartesian coordinates (x, y):

$x = r \cdot \cos(\theta)$

$y = r \cdot \sin(\theta)$

To be more precise, we can replace $r$ with the previous value:

$x = a \cdot \exp(k \cdot \theta) \cdot \cos(\theta)$

$y = a \cdot \exp(k \cdot \theta) \cdot \sin(\theta)$

These two equations are the key to our animation, and are wrapped in the *logarithmicSpiral* function.

const logarithmicSpiral = ({angle,index,}: {angle: number;index: number;}) => {'worklet'; // below you'll see why 👇const a = index / 4;const k = 0.005;return {x: a * Math.exp(k * angle) * Math.cos(angle * index),y: a * Math.exp(k * angle) * Math.sin(angle * index),};};

The theory is great, but how can we apply it to our animation?

It's all about the circles. We need to create a path with a lot of circles and position them according to the logarithmic spiral.

Every time the user will interact with the spiral, we'll need to update the angle. Then, the spiral coordinates will be updated according to the new angle.

Since the coordinates depend on the angle, we can use the *useDerivedValue* hook to create an array of coordinates.

As you can see, the *logarithmicSpiral* function is called inside the *useDerivedValue* hook. That's why we need to support the 'worklet' keyword.

const spiralCircleCount = 1500; // feel free to play with thisconst angle = useSharedValue(Math.PI / 2);const spiralCoordinates = useDerivedValue(() => {return Array.from({ length: spiralCircleCount }, (_, index) => {const { x, y } = logarithmicSpiral({angle: angle.value,index,});return { x, y };});});

Once we have the spiral coordinates, we can easily create a Skia Path. Theoretically, we could use the *useDerivedValue* hook, but we'll use the *usePathValue hook* because it ensures that the path updates in the most efficient way (thanks to Terry for the tip!).

const path = usePathValue(skPath => {'worklet';skPath.reset();for (let index = 0; index < spiralCircleCount; index++) {const x = spiralCoordinates.value[index].x;const y = spiralCoordinates.value[index].y;// x, y, radiusskPath.addCircle(x, y, 1);}return skPath;});

Right now, we have defined the base ingredients of our animation. But we don't have a visual representation of the spiral yet.

We can use the *Skia.Canvas* component to visualize the spiral.

<Canvas style={{ flex: 1 }}><Grouptransform={({translateX: windowWidth / 2,},{translateY: windowHeight / 2,})}><Path path={path} color={'white'} /></Group></Canvas>

To keep things simple, we can update the angle value with the onTouchEnd event from the View component.

<ViewonTouchEnd={() => {angle.value = Math.PI * 2 * Math.random();}}>{...PreviousCode}</View>

And there we go:

If you want to add a bit more visual interest to the spiral, you can add a SweepGradient.
Personally, I love to copy and paste the gradient from the Skia documentation.

<Group transform={rGroupTransform}><Path path={path} /><SweepGradient c={vec(0, 0)} colors={['cyan', 'magenta', 'yellow', 'cyan']} /></Group>

Usually, I add a BlurMask to the SweepGradient to make it look a bit more subtle (*optional*).

<Group transform={rGroupTransform}><Path path={path} /><SweepGradient c={vec(0, 0)} colors={['cyan', 'magenta', 'yellow', 'cyan']} /><BlurMask blur={5} style="solid" /> {/* 👈 */}</Group>

Animating 1500 circles is not an easy task, but as always, there is a trick!

The trick is to split the coordinates into two arrays: one for the x coordinates and one for the y coordinates.

Once we have the two arrays, we can animate them independently.

This is going to be extremely performant thanks to Reanimated's magic.

const animatedSpiralCoordinatesX = useDerivedValue(() => {return withTiming(spiralCoordinates.value.map(coordinate => {return coordinate.x;}),{duration: 1500,});});const animatedSpiralCoordinatesY = useDerivedValue(() => {return withTiming(spiralCoordinates.value.map(coordinate => {return coordinate.y;}),{duration: 1500,});});

Once we have the two arrays, we can replace the *spiralCoordinates* with the *animatedSpiralCoordinatesX* and *animatedSpiralCoordinatesY*.

const path = usePathValue(skPath => {'worklet';skPath.reset();for (let index = 0; index < spiralCircleCount; index++) {const x = animatedSpiralCoordinatesX.value[index];const y = animatedSpiralCoordinatesY.value[index];// x, y, radiusskPath.addCircle(x, y, 1);}return skPath;});

To add more visual interest to our spiral, we can implement a dynamic radius for each circle. This will make the spiral appear to taper off as it expands outwards, creating a more organic and visually appealing effect.

The key to this effect is to calculate the radius of each circle based on its distance from the center. We'll use Reanimated's *interpolate* function to smoothly transition the radius from larger near the center to smaller at the edges.

Here's how we can modify our path creation:

// Pythagorean theoremconst MAX_DISTANCE_FROM_CENTER = Math.sqrt((windowWidth / 2) ** 2 + (windowHeight / 2) ** 2);const path = usePathValue(skPath => {'worklet';skPath.reset();for (let index = 0; index < spiralCircleCount; index++) {const x = animatedSpiralCoordinatesX.value[index];const y = animatedSpiralCoordinatesY.value[index];const distanceFromCenter = Math.sqrt(x ** 2 + y ** 2);const radius = interpolate(distanceFromCenter,[0, MAX_DISTANCE_FROM_CENTER],[1, 0.1],Extrapolate.CLAMP);skPath.addCircle(x, y, radius);}return skPath;});

Let's break down what's happening here:

- We calculate the maximum possible distance from the center of the canvas. This will be used as a reference for our interpolation.
- For each circle, we calculate its distance from the center using the Pythagorean theorem.
- We use Reanimated's
*interpolate*function to determine the radius. This function maps the distance from the center to a radius value:- At the center (distance = 0), the radius will be 1.
- At the maximum distance, the radius will be 0.1.
- For all points in between, the radius will be smoothly interpolated.

- We use this calculated radius when adding each circle to our path.

The result is a spiral that starts bold at the center and gradually becomes more delicate towards the edges, creating a beautiful tapering effect.

In this article, we've explored how to create a mesmerizing animated spiral using React Native Skia and Reanimated. We've covered the mathematical foundations of the logarithmic spiral, implemented basic and gradient-based visualizations, added smooth animations, and finally, enhanced the visual appeal with a dynamic radius effect.

Feel free to experiment further with the provided code. You could try different color schemes, adjust the spiral parameters, or even add interactive elements like pinch-to-zoom or rotation gestures.

Every week I send out a newsletter sharing new things about React Native animations.