Home > Software engineering >  SVG continuous inwards square spiral animation with pure CSS/JS
SVG continuous inwards square spiral animation with pure CSS/JS

Time:05-23

I need some help with this kinda specific animation. It is a square spiral pattern that keeps going inwards until it's fully complete. I somewhat did manage to get it going but I don't know how to stop the animation properly, and I'm not sure if the math behind it is mostly efficient/correct.

Here's what I have for now:

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

I added the js function just to demonstrate how I'm generating the points. As you can see the animation plays exactly how I want, I just can't find a way to wrap it up properly. Also, I'm unsure if this function would generate correct points for varying width/height/strokeWidth.

I appreciate any help! Thanks in advance. :)

PS.: I could not find a mathematical term for this pattern (square-ish spiral) so I'm more than happy to learn how to call it properly.

Edit

Based on @enxaneta answers (thank you!) it seems I'm incorrectly calculating the max number of iterations. This can be seen whenever width !== height. I'll do some research on how I'm producing this value, maybe this formula isn't adequate to properly "stop" the animation without any blank space.

CodePudding user response:

I guess you also need to check if your current drawing position has already reached a maximum x/y (close to you center).

The calculation for the loops iterations works fine.
Currently you're drawing 4 new points in each step.

Depending on your stroke-width you might need to stop drawing e.g after the 2. or 3. point when you're close to the center X/Y coordinates.

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>

CodePudding user response:

To control the animation, instead of CSS, use the Web Animations API

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

  • Wrap all in a standard Web Component <svg-spiral> with shadowDOM, so you can have multiple components on screen without any global CSS conflicts.

  • set a pathLenght="100" on the polygon, so you don't have to do calculations

  • stroke-dasharray must be written as: strokeDasharray in WAAPI

  • The animation triggers an onfinish function

  • clicking an <svg-spiral> in the SO snippet below will restart the animation

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

  • Related