Animate svg with @keyframes on an elipsis path

As the title says, I'm trying to animate a svg across what would look like an ellipsis path to match that of the first icon(infinity symbol). I've tried using transform but the movement is awful. How could I achieve this in a smooth(as it can be) way?

I've found some articles about something similar but it doesn't get me the smooth ellipsis I'm after: https://www.useragentman.com/blog/2013/03/03/animating-circular-paths-using-css3-transitions/ http://thenewcode.com/860/Animating-Elements-In-Arcs-Circles-and-Ellipses-With-CSS https://usefulangle.com/post/32/moving-an-element-in-circular-path-with-css

Here is a link to a pen https://codepen.io/acubaniti/pen/abNqzEV?editors=1100

Here is the code :

#gesture {
  width: 300px;
  height: 300px;
}

#phone {
  fill: white;
  animation: phone-orbits-cw 6s cubic-bezier(0.5, 0, 0.38, 0.1) infinite;
  opacity: 0;
  transform: rotate3d(0, 0, 0) translate3d(0, 0, 0);
  will-change: transform, opacity;
  transform-style: preserve-3d;
  transform-origin: center top;
}

#left, #right {
  fill: none;
  stroke-width: 10;
  stroke-miterlimit: 10;
  stroke: white;
  transform-origin: 200px 150px;
  animation-fill-mode: forwards;
  stroke-dasharray: 314;
  stroke-dashoffset: 314;
  opacity: 0;
}

#right {
  animation: circle-fill 6s linear infinite;
  stroke: white;
  transform: rotate(-180deg);
}

#left {
  animation: circle-fill-left 6s linear infinite;
  stroke: white;
  transform: rotateX(180deg);
}

@keyframes circle-fill {
  0%, 12.5% {
    stroke-dashoffset: 314;
    opacity: 1;
  }
  12.5%, 100% {
    stroke-dashoffset: 0;
  }
  37.5% {
    opacity: 1;
  }
  49%, 100% {
    opacity: 0;
  }
}
@keyframes circle-fill-left {
  12.5% {
    stroke-dashoffset: 314;
    opacity: 1;
  }
  25% {
    stroke-dashoffset: 0;
  }
  37.5% {
    opacity: 1;
  }
  49%, 100% {
    opacity: 0;
    stroke-dashoffset: 0;
  }
}
@keyframes phone-orbits-cw {
  0% {
    opacity: 0;
    transform: rotate3d(0, 0, 0) translate3d(0, 0, 0);
  }
  45% {
    opacity: 0;
    transform: rotate3d(0, 0, 1, -360deg) translate3d(2%, -5%, 0) rotate3d(0, 0, 1, 360deg);
  }
  58.75% {
    opacity: 1;
    transform: rotate3d(0, 0, 1, -360deg) translate3d(2%, -5%, 0) rotate3d(0, 0, 1, 360deg);
  }
  72.5% {
    opacity: 1;
  }
  86.25% {
    opacity: 1;
    transform: rotate3d(0, 0, 1, 360deg) translate3d(-2%, -5%, 0) rotate3d(0, 0, 1, -360deg);
  }
  100% {
    opacity: 1;
    transform: rotate3d(0, 0, 1, -360deg) translate3d(0, 0, 0) rotate3d(0, 0, 1, 360deg);
  }
}
@keyframes phone-orbits-ccw {
  0% {
    opacity: 0;
    transform: rotate3d(0, 0, 0) translate3d(0, 0, 0);
  }
  40% {
    opacity: 0;
    transform: rotate3d(0, 0, 1, 180deg) translate3d(0, 0, 0) rotate3d(0, 0, 1, -180deg);
  }
  45% {
    opacity: 1;
  }
  50% {
    opacity: 1;
    transform: rotate3d(0, 0, 1, -180deg) translate3d(2%, -5%, 0) rotate3d(0, 0, 1, 180deg);
  }
  75% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    transform: rotate3d(0, 0, 1, 180deg) translate3d(0, 0, 0) rotate3d(0, 0, 1, -180deg);
  }
}
<html>
  <body style="background-color: black;"> 
    <svg id="gesture" viewBox="0 0 300 300" xml:space="preserve">
      <g id="phone">
        <path  d="M212.1 23.1H90.2c-4.8 0-8.8 3.9-8.8 8.8v236.4c0 4.8 3.9 8.8 8.8 8.8h121.9c4.8 0 8.8-3.9 8.8-8.8V31.8c0-4.8-4-8.7-8.8-8.7zm0 8.5c.1 0 .2.1.2.2v28.4H90.2V31.6h121.9zM90 268.2l.2-200.6h122.1l-.2 200.8-122.1-.2z"/>
        <circle cx="151.1" cy="248.2" r="8.8"/>
        <path d="M142 49.5h18.3c2.3 0 4.1-1.8 4.1-4.1s-1.8-4.1-4.1-4.1H142c-2.3 0-4.1 1.8-4.1 4.1s1.8 4.1 4.1 4.1z"/>
      </g>
      <circle id="right" cx="200" cy="150" r="50"/>
      <circle id="left" cx="100" cy="150" r="50"/>
    </svg> 
  </body>
</html>

Thank you.

Upvotes: 1

Views: 2851

Answers (1)

ccprog
ccprog

Reputation: 21821

If you can compromise on browser compatibility, you should be able to do this in a much more semantic way using a motion path. It needs Chrome 64, Edge 79, Firefox 72, Opera 45, but has no Safari support.

The following example simplifies your code in several ways. First, I have rewritten the two circles as a single path, and additionally moved it so that the start and end of the path (the mid-point of the "infinity" sign) is at the origin of the coordinate system. This is because this path will be reused as the motion path - not by quotation, that is unfortunately impossible if you stay with CSS, but so you can just copy-and-paste the path commands.

To make my life a bit easier with the stroke-dashoffset animation, the path also gets the attribute (not CSS property!) pathLength="100". This is basically an arbitrary value that means: for all distance-along-a-path computaions, act as if 100 was the path length.

Second, I defined an offset-path with the same path commands used. offset-rotate is set to 0deg to avoid the phone rotating along the path tangent while it moves. The offset-distance increases from 0 to 100% during the animation.

#gesture {
  width: 300px;
  height: 300px;
}

#phone {
  fill: white;
  animation: phone-orbit 6s ease-in-out infinite;
  opacity: 0;
  offset-path: path("M0 0 A 30 30 0 0 1 60 0 A 30 30 0 0 1 0 0 A 30 30 0 0 0 -60 0 A 30 30 0 0 0 0 0");
  offset-rotate: 0deg;
  offset-distance: 0;
  will-change: transform, opacity;
}

#infinity {
  fill: none;
  stroke-width: 6.666;
  stroke: white;
  animation-fill-mode: forwards;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  opacity: 0;
  animation: infinity-fill 6s linear infinite;
}

@keyframes infinity-fill {
  0%, 25% {
    stroke-dashoffset: 100;
    opacity: 1;
  }
  25%, 100% {
    stroke-dashoffset: 0;
  }
  37.5% {
    opacity: 1;
  }
  49%, 100% {
    opacity: 0;
  }
}
@keyframes phone-orbit {
  0% {
    opacity: 0;
  }
  40% {
    opacity: 0;
  }
  45% {
    opacity: 1;
  }
  50% {
    offset-distance: 0%;
  }
  100% {
    opacity: 1;
    offset-distance: 100%;
  }
}
<body style="background-color: black;"> 
    <svg id="gesture" viewBox="0 0 300 300" xml:space="preserve">
      <g id="phone">
        <path  d="M212.1 23.1H90.2c-4.8 0-8.8 3.9-8.8 8.8v236.4c0 4.8 3.9 8.8 8.8 8.8h121.9c4.8 0 8.8-3.9 8.8-8.8V31.8c0-4.8-4-8.7-8.8-8.7zm0 8.5c.1 0 .2.1.2.2v28.4H90.2V31.6h121.9zM90 268.2l.2-200.6h122.1l-.2 200.8-122.1-.2z"/>
        <circle cx="151.1" cy="248.2" r="8.8"/>
        <path d="M142 49.5h18.3c2.3 0 4.1-1.8 4.1-4.1s-1.8-4.1-4.1-4.1H142c-2.3 0-4.1 1.8-4.1 4.1s1.8 4.1 4.1 4.1z"/>
      </g>
      <path id="infinity" transform="translate(150 150) scale(1.666)" d="M0 0 A 30 30 0 0 1 60 0 A 30 30 0 0 1 0 0 A 30 30 0 0 0 -60 0 A 30 30 0 0 0 0 0"  pathLength="100"/>
    </svg> 
</body>

For better support, you can switch over to a in-markup SMIL animation. Only old IE versions will fail you there.

The difference is mostly in the way the different animations are started: apart from the first infinityFill animation being started at 0s, each subsequent animation is started in relation to its runtimes. Especially the phoneOrbit animation starts at the end of the infinityFill animation, then the end of phoneOrbit starts infinityFill again, and so on in infinity.

Additionally, the motion path can be truely reused.

#gesture {
  width: 300px;
  height: 300px;
}

#phone {
  fill: white;
  opacity: 0;
  will-change: transform, opacity;
}

#infinity {
  fill: none;
    stroke-width: 10;
    stroke: white;
    stroke-dasharray: 100;
  opacity: 0;
}
<body style="background-color: black;"> 
    <svg id="gesture" viewBox="0 0 300 300">
      <defs>
        <path id="motion" d="M0 0 A 30 30 0 0 1 60 0 A 30 30 0 0 1 0 0 A 30 30 0 0 0 -60 0 A 30 30 0 0 0 0 0"  pathLength="100"/>
      </defs>
      <g id="phone">
        <path  d="M212.1 23.1H90.2c-4.8 0-8.8 3.9-8.8 8.8v236.4c0 4.8 3.9 8.8 8.8 8.8h121.9c4.8 0 8.8-3.9 8.8-8.8V31.8c0-4.8-4-8.7-8.8-8.7zm0 8.5c.1 0 .2.1.2.2v28.4H90.2V31.6h121.9zM90 268.2l.2-200.6h122.1l-.2 200.8-122.1-.2z"/>
        <circle cx="151.1" cy="248.2" r="8.8"/>
        <path d="M142 49.5h18.3c2.3 0 4.1-1.8 4.1-4.1s-1.8-4.1-4.1-4.1H142c-2.3 0-4.1 1.8-4.1 4.1s1.8 4.1 4.1 4.1z"/>
        <animateMotion id="phoneOrbit" dur="3s"
                       begin="infinityFill.end" rotate="0">
          <mpath href="#motion" />
        </animateMotion>
        <animate attributeName="opacity" dur="6s"
                 begin="infinityFill.begin"
                 values="0;0;1;1" keyTimes="0;.45;.5;1" />
      </g>
      <use id="infinity" href="#motion" transform="translate(150 150) scale(1.666)">
        <animate id="infinityFill" attributeName="stroke-dashoffset"
                 dur="3s" begin="0s;phoneOrbit.end"
                 values="100;0;0" keyTimes="0;.5;1" />
        <animate attributeName="opacity" dur="3s"
                 begin="infinityFill.begin"
                 values="1;1;0" keyTimes="0;.75;1" />
      </use>
    </svg> 
</body>

Upvotes: 1

Related Questions