在省略号路径上使用@keyframes 动画 svg

Animate svg with @keyframes on an elipsis path

正如标题所说,我正在尝试为一个 svg 制作动画,使其跨越看起来像省略号的路径以匹配它 第一个图标(无限符号)。我试过使用变换,但运动很糟糕。我怎样才能顺利(尽可能地)实现这一目标?

我找到了一些关于类似内容的文章,但它并没有让我得到我想要的平滑省略号: 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

这里是link来一支笔https://codepen.io/acubaniti/pen/abNqzEV?editors=1100

代码如下:

#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>

谢谢。

如果您可以在浏览器兼容性方面做出妥协,您应该能够使用 motion path 以更加语义化的方式做到这一点。它需要 Chrome 64、Edge 79、Firefox 72、Opera 45,但不支持 Safari。

以下示例以多种方式简化了您的代码。首先,我将这两个圆圈重写为一条路径,并另外移动它,使路径的起点和终点(“无穷大”符号的 mid-point)位于坐标系的原点。这是因为这条路径将被重用为运动路径——而不是引用,不幸的是,如果你继续使用 CSS,那是不可能的,但你可以只 copy-and-paste 路径命令。

为了让 stroke-dashoffset 动画更轻松一些,路径也获得了属性(不是 CSS 属性!)pathLength="100"。这基本上是一个任意值,意味着:对于所有 distance-along-a-path 计算, 就好像 100 是路径长度。

其次,我使用相同的路径命令定义了一个 offset-pathoffset-rotate 设置为 0deg 以避免 phone 在移动时沿路径切线旋转。 offset-distance 在动画期间从 0% 增加到 100%。

#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>

为了获得更好的支持,您可以切换到 in-markup SMIL animation。只有旧的 IE 版本会让你失望。

主要区别在于不同动画的启动方式:除了第一个 infinityFill 动画在 0 秒启动外,每个后续动画的启动都与其运行时间有关。特别是 phoneOrbit 动画在 infinityFill 动画结束时开始,然后 phoneOrbit 结束再次开始 infinityFill,以此类推。

此外,运动路径可以真正重复使用。

#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>