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.
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⋅exp(k⋅θ)
Where r is the radius, a is the constant, k is the growth factor and θ 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⋅cos(θ)
y=r⋅sin(θ)
To be more precise, we can replace r with the previous value:
x=a⋅exp(k⋅θ)⋅cos(θ)
y=a⋅exp(k⋅θ)⋅sin(θ)
These two equations are the key to our animation, and are wrapped in the logarithmicSpiral function.
constlogarithmicSpiral=({
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 Circles Path
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.
The spiral coordinates
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 this
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, radius
skPath.addCircle(x, y,1);
}
return skPath;
});
Visualizing the Spiral
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.
<Canvasstyle={{ flex:1}}>
<Group
transform={
({
translateX: windowWidth /2,
},
{
translateY: windowHeight /2,
})
}
>
<Pathpath={path}color={'white'}/>
</Group>
</Canvas>
To keep things simple, we can update the angle value with the onTouchEnd event from the View component.
<View
onTouchEnd={()=>{
angle.value=Math.PI*2*Math.random();
}}
>
{...PreviousCode}
</View>
And there we go:
Sweep Gradient is your friend
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.
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, radius
skPath.addCircle(x, y,1);
}
return skPath;
});
Final touches
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 theorem
constMAX_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.
Conclusion
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.
Join my weekly newsletter
Every week I send out a newsletter sharing new things about React Native animations.