tldr:
/********* CORRECT *********/
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/********* NOT CORRECT *********/
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(359deg); }
}
full explanation:
The following syntaxes are equivalent and all produce a perfectly smooth and continuous infinite circular animation:
@keyframes spin-turn {
from { transform: rotate(0turn); }
to { transform: rotate(1turn); }
}
@keyframes spin-grad {
from { transform: rotate(0grad); }
to { transform: rotate(400grad); }
}
@keyframes spin-deg {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin-rad {
from { transform: rotate(0rad); }
to { transform: rotate(6.28318530718rad); } /* 2 × π */
}
Most importantly, there is no animation step in between 100% and 0%.
When transitioning between animation iterations, the frame corresponding to 100% is not rendered. The browser uses its value to calculate all the intermediary frames up to it (their number depends on device refresh rate and browser rendering engine, not on the CSS unit used). When the next animation iteration exists, the frame corresponding to 100% is replaced by the first frame of the next iteration (the 0% one).
The 100% frame is only rendered when animation-iteration-count is a positive finite number, after all iteration counts have been played. In other words, when the animation has stopped.
Thus, using to: { transform: rotate(359deg); } produces a jump forward in the animation of exactly 1deg. It's a jump equal to 0.2(7)% of a full turn and is therefore hardly noticeable when the animated element is small or when the speed of animation is relatively fast.
Here's a tool to compare the different syntaxes. The jump forward can be observed at the top position (when the radar line reaches the north point of the circle), with deg-359 animation syntax selected, with an animation duration above 10 seconds.
new Vue({
el: '#app',
data: () => ({
units: ['turn', 'grad', 'rad', 'deg', 'deg-359'],
animation: 'spin-deg-359',
duration: 18
}),
computed: {
rotatorStyle() {
return {
animation: `${this.animation} ${this.duration}s linear infinite`
}
}
}
})
body {
margin: 0;
}
* { box-sizing: border-box; }
.controls > div {
display: flex;
padding: 3px 0;
}
.controls {
padding: 1rem;
}
.controls label {
width: 80px;
}
input[type="number"] {
width: 73px;
}
#app {
height: 100vh;
padding: 1rem;
display: flex;
align-items: flex-start;
justify-content: space-evenly;
overflow: hidden;
}
.ratio {
display: grid;
width: calc(100vh - 2rem);
}
@media(max-width: 767.98px) {
#app {
flex-direction: column;
height: auto;
}
.ratio {
width: 100%;
flex: 0 0 auto;
}
}
.ratio > * {
grid-area: 1/1;
}
.rotator {
display: flex;
border: 1px solid red;
border-radius: 50%;
align-items: center;
justify-content: center;
position: relative;
}
.rotator:before {
content: '';
position: absolute;
left: 50%;
width: 50%;
height: 0;
top: 50%;
transform: rotate(-90deg);
transform-origin: 0 0;
border-bottom: 1px solid red;
}
@keyframes spin-deg {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin-deg-359 {
from { transform: rotate(0deg); }
to { transform: rotate(359deg); }
}
@keyframes spin-rad {
from { transform: rotate(0rad); }
to { transform: rotate(6.28318530718rad); }
}
@keyframes spin-turn {
from { transform: rotate(0turn); }
to { transform: rotate(1turn); }
}
@keyframes spin-grad {
from { transform: rotate(0grad); }
to { transform: rotate(400grad); }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="controls">
<div>
<label for="duration">duration</label>
<input id="duration" v-model="duration" type="number">s
</div>
<div>
<label for="animation">animation</label>
<select id="animation" v-model="animation">
<option v-for="unit in units" :key="unit" :value="'spin-' + unit">{{unit}}</option>
</select>
</div>
</div>
<div class="ratio">
<svg viewBox="0 0 1 1"></svg>
<div class="rotator"
:style="rotatorStyle"></div>
</div>
</div>
The jump becomes more noticeable as you increase the screen size (full-page mode) or as you increase the animation-duration (i.e: 30s).