Move an element along a circle segment

Motivation

I was trying to make an element appear to move along a circular path. I needed this for the folding animation of the backside of the box on my website's start page.

Problem

We can use the translate property to change the position of the element. In this example, hovering the parent element moves the element vertically and horizontally by 100px. The element will move along a straight line from point 1 to point 2. The goal is to make the element with the blue border follow the green circle segment.

.element {
transition: translate 1s linear;
}
.area:hover .element {
translate: 100px 100px;
}
1
2
Move your cursor over the grey area or tap on it to initiate the animation.

Approach 1: Animation with pre-calculated positions

We can use a @keyframe animation to define some points the element should follow along when moving. The element will only follow approximately along the green circle because it still moves in a straight line from one defined point to the next.

If your trigonometry is as rusty as mine: To calculate the x coordinate we use the sinus, i.e. with JavaScript Math.sin(segment_size * 2 * Math.PI * keyframe_progress) * total_translate, where segement_size is the fraction of the circle we want to move along (i.e. for the quarter circle in this example it is 0.25), keyframe_progress is the percentage of the keyframe (i.e. 0.33) and total_translate is the final translated position (i.e. 100).
For the y coordinate we use the cosinus, i.e. w (1 - Math.cos(segment_size * 2 * Math.PI * keyframe_progress)) * total_translate

@keyframes approx-circular-path {
0% {
translate: 0px 0px;
}
33% {
translate: 49.54px 13.13px;
}
66% {
translate: 86.07px 49.09px;
}
100% {
translate: 100px 100px;
}
}
.area:hover .element {
animation: 1s linear forward approx-circular-path;
}
1
2
33%
66%

Depending on how fast the animation runs, the distance the element should move and how many keyframes are pre-calculated the result may be more or less satisfying. In this example I calculated only two intermediate keyframes at 33% and 66% of the animation duration (their position is shown as circles on the arc), so the deviation from the desired path is easier to observe.

Approach 2: Animation with positions calculated with CSS

Instead of pre-calculating the values, we could use CSS trigonometry functions. At point of writing these functions are supported in Firefox and Safari. We still have to define a sufficient number of keyframes in order to get a smooth animation.

.area {
--radius: 100px;
}
@keyframes approx-circular-path {
0% {
translate: 0px 0px;
}
33% {
translate: calc(var(--radius) * sin(0.33 * 90deg)) calc(var(--radius) * (1 - cos(0.33 * 90deg)));
}
66% {
translate: calc(var(--radius) * sin(0.66 * 90deg)) calc(var(--radius) * (1 - cos(0.66 * 90deg)));
}
100% {
translate: var(--radius) var(--radius);
}
}
.area:hover .element {
animation: 1s linear forwards trigonometry-circular-path;
}
Your browser doesn't support CSS trigonometry functions. This animation will not work as intended.
1
2
33%
66%

If your browser supports trigonometry functions in CSS, the result will be identical to that from Approach 1

Approach 3: Combine animations with timing functions

In the two previous shown approaches I used the linear timing functions for the animations. We want to keep that, i.e. the element should not ease in or out. But if we look at the movement along x- and y-axis separately, the speed on each axis is not linear. We can define one animation with fitting animation-timing-function: cubic-bezier(..) for the x-axis, and a second one for the y-axis and then combine them with the animation-composition: add property.

Please be aware that animation-composition is still an experimental technology. It is supported in Safari since version 16 and in Firefox since version 104 behind a feature flag.

The big challenge here is to find cubic bezier functions that closely describe our circle segment. After skimming through an article on the subject, not understanding any of it and a bit of experimentation, I ended with these parameters for the x-axis cubic-bezier(0.11, .4, .6, .89) and the inverse cubic-bezier(.4, 0.11, .89, .6) for the y-axis.

@keyframes composition-circular-path-x {
0% {
translate: 0px 0px;
}
100% {
translate: var(--radius) 0px;
}
}
@keyframes composition-circular-path-y {
0% {
translate: 0px 0px;
}
100% {
translate: 0px var(--radius);
}
}
.element {
animation: 1s cubic-bezier(0.11, .4, .6, .89) forwards composition-circular-path-x,
1s cubic-bezier(.4, 0.11, .89, .6) forwards composition-circular-path-y;
animation-composition: add;
}
Your browser doesn't support animation composition. This animation will not work as intended.
1
2

The resulting movement of the element is not perfectly linear — it's a bit faster in the beginning and then slows down — but the element follows nicely along the green path.

Conclusion

So, which of the 3 approaches detailed above should be preferred?
Considering the still poor support for CSS trigonometry functions and the animation-composition property I would recommend for now to use approach 1 as default and wrapped in an @supports rule as an progressive enhancement approach 3: See Update below.

@keyframes approx-circular-path { /* see above */ }
@keyframes composition-circular-path-x { /* see above */ }
@keyframes composition-circular-path-y { /* see above */ }

.area:hover .element {
animation: 1s linear forward approx-circular-path;
}

@supports (animation-composition: add) {
.area:hover .element {
animation: 1s cubic-bezier(0.11, .4, .6, .89) forwards composition-circular-path-x,
1s cubic-bezier(.4, 0.11, .89, .6) forwards composition-circular-path-y;
animation-composition: add;
}

}

I focused here only on HTML and CSS. Using SVG or JavaScript you could probably find other solutions. Please let me know if you have any additional ideas, feedback or improvements!

Update, 2022-12-17 22:20

Ana Tudor pointed out on Mastodon how to properly do this by chaining transform operations. She suggests to first rotate and then translate the element. To make this animation match those from my previous examples, I rotate the element back after translating. Thanks Ana!

@keyframes circular-rotate {
from {
transform:
rotate(-90deg)
translate(100px)
rotate(0deg);
}
to {
transform:
rotate(0deg)
translate(100px)
rotate(-90deg);
}
}

.element {
animation: 2s linear infinite circular-rotate;
}
1
2

This article has been published on on my blog. Here's a list of all articles if you're interested in this kind of stuff.

Tags: til,CSS,Animation