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

8nuwlpux  于 2023-05-30  发布在  其他
关注(0)|答案(2)|浏览(145)

我需要一些帮助这种特殊的动画。它是一个正方形的螺旋图案,不断向内,直到它完全完成。我确实设法让它继续运行,但我不知道如何正确地停止动画,我不确定它背后的数学是否有效/正确。
以下是我目前的情况:

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函数只是为了演示如何生成点。正如你所看到的动画播放正是我想要的,我只是找不到一种方法来 Package 它正确。此外,我不确定这个函数是否会为不同的宽度/高度/strokeWidth生成正确的点。
我感谢任何帮助!先谢谢你了。:)
附言:我找不到一个数学术语来描述这种模式(方形螺旋),所以我很乐意学习如何正确地称呼它。

编辑

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

von4xj4u

von4xj4u1#

我猜你还需要检查你当前的绘图位置是否已经达到了最大x/y(接近你的中心)。
循环迭代的计算工作正常。
目前,您在每一步中绘制4个新点。
根据您的stroke-width,您可能需要停止绘制,例如在2.或3.当你接近中心X/Y坐标时点。

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

nom7f22z2#

要控制动画,请使用Web Animations API,而不是CSS

  • https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API
  • 使用shadowDOM将所有组件 Package 在一个标准的Web Component(JSWC)<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>

相关问题