SVG连续向内方形螺旋动画纯CSS/JS

SVG continuous inwards square spiral animation with pure CSS/JS

我需要一些关于这种特殊动画的帮助。它是一个方形螺旋图案,一直向内延伸,直到完全完成。我确实设法让它继续运行,但我不知道如何正确停止动画,而且我不确定它背后的数学是否主要是 efficient/correct.

这是我目前拥有的:

function createSquareSpiralPath(
  strokeWidth,
  width,
  height,
) {
  const maxIterations = Math.trunc(Math.min(width, height) / 2 / strokeWidth); // ???
  let path = '';
  for (let i = 0; i <= maxIterations; i++) {
    const step = strokeWidth * i;
    const computed = [
      `${step},${height - step}`,
      `${step},${step}`,
      `${width - step - strokeWidth},${step}`,
      `${width - step - strokeWidth},${height - step - strokeWidth} `,
    ];
    path += computed.join(' ');
  }

  return path.trim();
}
.spiral {
  stroke-dasharray: 6130;
  stroke-dashoffset: 6130;
  animation: moveToTheEnd 5s linear forwards;
}

@keyframes moveToTheEnd {
  to {
    stroke-dashoffset: 0;
  }
}
<svg viewBox="-10 -10 350 350" height="350" width="350">
  <polyline class="spiral" points="
  0,350 0,0 330,0 330,330 20,330 20,20 310,20 310,310 40,310 40,40 290,40 290,290 60,290 60,60 270,60 270,270 80,270 80,80 250,80 250,250 100,250 100,100 230,100 230,230 120,230 120,120 210,120 210,210 140,210 140,140 190,140 190,190 160,190 160,160 170,160 170,170"
  style="fill:transparent;stroke:black;stroke-width:20" />
  Sorry, your browser does not support inline SVG.
</svg>

我添加了 js 函数只是为了演示我是如何生成点的。如您所见,动画完全按照我想要的方式播放,但我只是找不到正确包装它的方法。另外,我不确定这个函数是否会为不同的 width/height/strokeWidth.

生成正确的点

感谢您的帮助!提前致谢。 :)

PS.: 我找不到这个模式的数学术语(方形螺旋)所以我很高兴学习如何正确调用它。

编辑

根据@enxaneta 的回答(谢谢!),我似乎错误地计算了最大迭代次数。这可以在 width !== height 时看到。我会研究一下我是如何产生这个值的,也许这个公式不足以正确地“停止”没有任何空白的动画 space.

要控制动画,而不是 CSS,请使用网络动画 API

  • https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API

  • 使用 shadowDOM 将所有内容包装在标准 Web 组件中 <svg-spiral>,这样您就可以在屏幕上显示多个组件,而不会出现任何全局 CSS 冲突。

  • 在多边形上设置一个pathLenght="100",这样就不用计算了

  • stroke-dasharray在WAAPI

    中必须写成:strokeDasharray
  • 动画触发onfinish函数

  • 单击下面 SO 片段中的 <svg-spiral> 将重新启动动画

<div style="display:grid;grid:1fr/repeat(4,1fr)">
  <svg-spiral></svg-spiral>
  <svg-spiral stroke="rebeccapurple" width="1000" strokewidth="10"></svg-spiral>
  <svg-spiral stroke="blue" duration="10000"></svg-spiral>
  <svg-spiral stroke="red" width="6000" duration="1e4"></svg-spiral>
</div>
<script>
  customElements.define("svg-spiral", class extends HTMLElement {
    connectedCallback() {
      let strokewidth = this.getAttribute("strokewidth") || 30;
      let width = this.getAttribute("width") || 500; let height = this.getAttribute("height") || width;
      let points = '';
      for (let i = 0; i <= ~~(Math.min(width, height) / 2 / strokewidth); i++) {
        const step = strokewidth * i;
        points += `${step},${height - step} ${step},${step} ${width - step - strokewidth},${step} ${width - step - strokewidth},${height - step - strokewidth}  `;
      }
      this.attachShadow({mode:"open"}).innerHTML = `<svg viewBox="-${strokewidth/2}-${strokewidth/2} ${width} ${height}"><polyline class="spiral" pathLength="100" points="${points}z"
            fill="transparent" stroke-width="${strokewidth}" /></svg>`;
      this.onclick = (e) => this.animate();
      this.animate();
    }
    animate() {
      let spiral = this.shadowRoot.querySelector(".spiral");
      spiral.setAttribute("stroke", this.getAttribute("stroke") || "black");
      let player = spiral.animate(
        [{ strokeDashoffset: 100, strokeDasharray: 100, opacity: 0 }, 
         { strokeDashoffset: 0,   strokeDasharray: 100, opacity: 1 }], 
         {
          duration: ~~(this.getAttribute("duration") || 5000),
          iterations: 1
        });
      player.onfinish = (e) => { spiral.setAttribute("stroke", "green") }
    }
  })
</script>

我想你还需要检查你当前的绘图位置是否已经达到最大值x/y(靠近你的中心)。

循环迭代的计算工作正常。
目前您在每一步绘制 4 个新点。

根据您的 stroke-width,您可能需要停止绘制,例如,当您靠近中心 X/Y 坐标时,在 2. 或 3. 点之后。

let spiral1 = createSquareSpiralPath(50, 500, 1000);
let spiral1_2 = createSquareSpiralPath(20, 1000, 500);
let spiral2 = createSquareSpiralPath(150, 300, 300);


function createSquareSpiralPath(strokeWidth, width, height) {
  let maxIterations = Math.trunc(Math.min(width, height) / 2 / strokeWidth); 
  let coords = [];

  //calculate max X/Y coordinates according to stroke-width 
  let strokeToWidthRatio = width * 1 / strokeWidth;
  let strokeToHeightRatio = height * 1 / strokeWidth;
  let maxX = (width - strokeWidth / strokeToWidthRatio) / 2;
  let maxY = (height - strokeWidth / strokeToHeightRatio) / 2;

  for (let i = 0; i <= maxIterations; i++) {
    const step = strokeWidth * i;
    // calculate points in iteration    
    let [x1, y1] = [step, (height - step)];
    let [x2, y2] = [step, step];
    let [x3, y3] = [(width - step - strokeWidth), step];
    let [x4, y4] = [(width - step - strokeWidth), (height - step - strokeWidth)];

    //stop drawing if max X/Y coordinates are reached 
    if (x1 <= maxX && y1 >= maxY) {
      coords.push(x1, y1)
    }
    if (x2 <= maxX && y2 <= maxY) {
      coords.push(x2, y2)
    }
    if (x3 >= maxX && y3 <= maxY) {
      coords.push(x3, y3)
    }
    if (x4 >= maxX && y4 >= maxY) {
      coords.push(x4, y4)
    }
  }
  let points = coords.join(' ');

  //calc pathLength from coordinates
  let pathLength = 0;
  for (let i = 0; i < coords.length - 2; i += 2) {
    let x1 = coords[i];
    let y1 = coords[i + 1];
    let x2 = coords[i + 2];
    let y2 = coords[i + 3];
    let length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    pathLength += length;
  }

  //optional: render svg
  renderSpiralSVG(points, pathLength, width, height, strokeWidth);
  return [points, pathLength];
}


function renderSpiralSVG(points, pathLength, width, height, strokeWidth) {
  const ns = "http://www.w3.org/2000/svg";
  let svgTmp = document.createElementNS(ns, "svg");
  svgTmp.setAttribute(
    "viewBox", [-strokeWidth / 2, -strokeWidth / 2, width, height].join(" ")
  );
  let newPolyline = document.createElementNS(ns, "polyline");
  newPolyline.classList.add("spiral");
  newPolyline.setAttribute("points", points);
  svgTmp.appendChild(newPolyline);
  document.body.appendChild(svgTmp);

  newPolyline.setAttribute(
    "style",
    `fill:transparent;
    stroke:black;
    stroke-linecap: square;
    stroke-width:${strokeWidth}; 
    stroke-dashoffset: ${pathLength};
    stroke-dasharray: ${pathLength};`
  );
}
svg {
  border: 1px solid red;
}

svg {
  display: inline-block;
  height: 20vw;
}

.spiral {
  stroke-width: 1;
  animation: moveToTheEnd 1s linear forwards;
}

.spiral:hover {
  stroke-width: 1!important;
}

@keyframes moveToTheEnd {
  to {
    stroke-dashoffset: 0;
  }
}
<p> Hover to see spiral lines</p>