Как синхронизировать преобразование SVG с помощью JavaScript

В этом примере кнопка svg-plus при нажатии превращается в кнопку закрытия и наоборот при нажатии svg.setAttribute("transform","rotate(45)");.

Можно ли так распланировать эту трансформацию, чтобы это был не просто переход из одного состояния в другое, а плавный переход?

Вот пример:

function rotate() {
  var svg = document.getElementById("svgID");
  if (svg.getAttribute("transform") === "rotate(45)") {
    svg.setAttribute("transform", "rotate(0)");
  } else {
    svg.setAttribute("transform", "rotate(45)");
  }
}
#button {
  margin: 0;
  padding: 0;
  background: none;
  outline: none;
  box-shadow: none;
  border: none;
}
<button id = "button" onclick = "rotate()">
        <svg id = "svgID"
        width = "100%"
        height = "100%"
        viewBox = "0 0 142.40739 142.40991"
        version = "1.1"
        xmlns = "http://www.w3.org/2000/svg"
        xmlns:svg = "http://www.w3.org/2000/svg">
       <defs
          id = "defs6" />
        <path
          id = "rect234"
          style = "display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-miterlimit:0;stroke-dasharray:none;paint-order:stroke fill markers"
          d = "m 85.223081,28.929132 a 4.2333333,4.2333333 0 0 0 -4.233333,4.233333 v 62.801852 l -65.486442,-0.24339 a 4.107912,4.107912 0 0 0 -4.247803,3.96255 4.2333333,4.2333333 0 0 0 4.217313,4.249353 l 65.516932,0.24339 v 62.9295 a 4.107912,4.107912 0 0 0 3.978568,4.23333 4.2333333,4.2333333 0 0 0 4.233334,-4.23333 v -62.89901 l 60.21338,0.22376 a 4.107912,4.107912 0 0 0 4.2478,-3.96255 4.2333333,4.2333333 0 0 0 -4.21731,-4.248833 L 89.20165,95.994807 V 33.162465 a 4.107912,4.107912 0 0 0 -3.978569,-4.233333 z"
          transform = "translate(-11.255471,-28.929132)" />
        </svg>
     </button>

преобразование SVG с помощью setAttribute("transform",rotate(45)"):

🤔 А знаете ли вы, что...
JavaScript поддерживает работу с куки и хранилищем веб-браузера для сохранения данных на клиентской стороне.


1
74
4

Ответы:

Вам нужно использовать свойство CSS animate.

Ниже показана односторонняя/одноразовая анимация. (после щелчка он не будет возвращаться или анимироваться дальше)

let svg = document.querySelector("#svg")
svg.addEventListener("click", () => {
  if (!svg.classList.contains("animateSVG")) {
    svg.classList.add("animateSVG")
  }
})
#container {
    position: absolute;
    top: 0px;
    left: 0px;
    height: 100px;
    width: 100px;
    border: 1px solid black;
}
@keyframes frames {
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(45deg);
    }
}

.animateSVG {
    animation-name: frames;
    animation-duration: 1s;
    animation-fill-mode: forwards;
}
<div id = "container">
    <svg id = "svg" viewBox = "0 0 3 3" version = "1.1" xmlns = "http://www.w3.org/2000/svg" xmlns:svg = "http://www.w3.org/2000/svg">
        <path style = "opacity: 0.468379; fill: none; stroke: #000000; stroke-width: 0.264583; stroke-linecap: round; stroke-linejoin: round;" d = "M0.5,1.5h2zM1.5,0.5v2z" />
    </svg>
</div>

Решено

Способ 1: requestAnimationFrame

function anim(from, to, cb, duration=100) {
  let startms;
  const frame = ms => {
    const x = (ms - startms) ;
    if (x > duration) {
      return cb(to);
    }
    cb(x / duration * (to - from) + from)
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(ms => {
    startms = ms;
    requestAnimationFrame(frame);
  })
}

function rotate() {
  var svg = document.getElementById("svgID");
  if (svg.getAttribute("transform") === "rotate(45)") {
    anim(45, 0, v=> svg.setAttribute("transform", `rotate(${v})`));
  } else {
    anim(0, 45, v=> svg.setAttribute("transform", `rotate(${v})`));
  }
}
#button {
  margin: 0;
  padding: 0;
  background: none;
  outline: none;
  box-shadow: none;
  border: none;
}
<button id = "button" onclick = "rotate()">
        <svg id = "svgID"
        width = "100%"
        height = "100%"
        viewBox = "0 0 142.40739 142.40991"
        version = "1.1"
        xmlns = "http://www.w3.org/2000/svg"
        xmlns:svg = "http://www.w3.org/2000/svg">
       <defs
          id = "defs6" />
        <path
          id = "rect234"
          style = "display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-miterlimit:0;stroke-dasharray:none;paint-order:stroke fill markers"
          d = "m 85.223081,28.929132 a 4.2333333,4.2333333 0 0 0 -4.233333,4.233333 v 62.801852 l -65.486442,-0.24339 a 4.107912,4.107912 0 0 0 -4.247803,3.96255 4.2333333,4.2333333 0 0 0 4.217313,4.249353 l 65.516932,0.24339 v 62.9295 a 4.107912,4.107912 0 0 0 3.978568,4.23333 4.2333333,4.2333333 0 0 0 4.233334,-4.23333 v -62.89901 l 60.21338,0.22376 a 4.107912,4.107912 0 0 0 4.2478,-3.96255 4.2333333,4.2333333 0 0 0 -4.21731,-4.248833 L 89.20165,95.994807 V 33.162465 a 4.107912,4.107912 0 0 0 -3.978569,-4.233333 z"
          transform = "translate(-11.255471,-28.929132)" />
        </svg>
     </button>

Способ 2: анимировать

function anim(el, from, to, duration=100) {
  const animation = [
    { transform: `rotate(${from}deg)` },
    { transform: `rotate(${to}deg)` }
  ];
  const timing = {
    fill: 'both',
    duration,
    iterations: 1
  };
  el.animate(animation, timing);
}

function rotate() {
  var svg = document.getElementById("svgID");
  if (svg.dataset.rotated === "1") {
    svg.dataset.rotated = "0";
    anim(svg, 45, 0);
  } else {
    svg.dataset.rotated = "1";
    anim(svg, 0, 45);
  }
}
#button {
  margin: 0;
  padding: 0;
  background: none;
  outline: none;
  box-shadow: none;
  border: none;
}
<button id = "button" onclick = "rotate()">
        <svg id = "svgID"
        width = "100%"
        height = "100%"
        viewBox = "0 0 142.40739 142.40991"
        version = "1.1"
        xmlns = "http://www.w3.org/2000/svg"
        xmlns:svg = "http://www.w3.org/2000/svg">
       <defs
          id = "defs6" />
        <path
          id = "rect234"
          style = "display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-miterlimit:0;stroke-dasharray:none;paint-order:stroke fill markers"
          d = "m 85.223081,28.929132 a 4.2333333,4.2333333 0 0 0 -4.233333,4.233333 v 62.801852 l -65.486442,-0.24339 a 4.107912,4.107912 0 0 0 -4.247803,3.96255 4.2333333,4.2333333 0 0 0 4.217313,4.249353 l 65.516932,0.24339 v 62.9295 a 4.107912,4.107912 0 0 0 3.978568,4.23333 4.2333333,4.2333333 0 0 0 4.233334,-4.23333 v -62.89901 l 60.21338,0.22376 a 4.107912,4.107912 0 0 0 4.2478,-3.96255 4.2333333,4.2333333 0 0 0 -4.21731,-4.248833 L 89.20165,95.994807 V 33.162465 a 4.107912,4.107912 0 0 0 -3.978569,-4.233333 z"
          transform = "translate(-11.255471,-28.929132)" />
        </svg>
     </button>

В этом конкретном случае (переключение между двумя состояниями) вы также можете рассмотреть более традиционный подход:

  • назначать развернутые/свернуть классы через JavaScript
  • анимировать с помощью CSS-переходов

let btns = document.querySelectorAll('.buttonClose');
btns.forEach(btn=>{
  btn.addEventListener('click', (e)=>{
    btn.classList.toggle('expanded');
  })
})
body{
font-size: 20em
}

.button {
  margin: 0;
  padding: 0;
  background: none;
  outline: none;
  box-shadow: none;
  border: 1px solid #000;
  width:1em;
  font-size:1em;
  line-height:0.5em;
  padding:0;
  margin:0;
}

.closeIcon{
  transform-origin: center;
  transition: 0.3s transform ease-in-out;
  fill: currentColor;
  height:100%;
  width:auto;
}

.expanded .closeIcon{
  transform: rotate(45deg)
}

.button2{
color:red;
font-size:0.5em;
}
<button class = "button buttonClose">
  <svg class = "closeIcon" viewBox = "0 0 143 143">
    <path d = "M74.25 0.5 a4.23 4.23 0 00-4.23 4.23v62.8l-65.49-.24a4.11 4.11 0 00-4.24 3.96 4.23 4.23 0 004.21 4.25l65.52 .25v62.93a4.11 4.11 0 003.98 4.23 4.23 4.23 0 004.23-4.23v-62.9l60.22 .22a4.11 4.11 0 004.24-3.96 4.23 4.23 0 00-4.21-4.25l-60.25-.23v-62.83a4.11 4.11 0 00-3.98-4.23z" />
  </svg>
</button>

<button class = "button buttonClose button2">
  <svg class = "closeIcon"  viewBox = "0 0 143 143">
    <path d = "M74.25 0.5 a4.23 4.23 0 00-4.23 4.23v62.8l-65.49-.24a4.11 4.11 0 00-4.24 3.96 4.23 4.23 0 004.21 4.25l65.52 .25v62.93a4.11 4.11 0 003.98 4.23 4.23 4.23 0 004.23-4.23v-62.9l60.22 .22a4.11 4.11 0 004.24-3.96 4.23 4.23 0 00-4.21-4.25l-60.25-.23v-62.83a4.11 4.11 0 00-3.98-4.23z" />
  </svg>
</button>

Оптимизируя данные пути, вы также можете избавиться от некоторых ненужных преобразований, упростив поворотный переход.

Вы можете использовать, например, svg-path-editor, чтобы уменьшить точность с плавающей запятой и центрировать путь в viewBox. Таким образом, мы можем опустить атрибут transform = "translate(-11.255471,-28.929132)", поскольку мы можем изменить положение данных относительного пути, изменив первые координаты M.

Нам также не нужны эти атрибуты:

  • version = "1.1" => ... совершенно бесполезно, возможно, требуется валдаторами
  • xmlns = "http://www.w3.org/2000/svg" => необходимо для SVG с внешними ссылками, но не требуется для встроенных элементов SVG

Кроме того, мы можем/должны удалить атрибут встроенного стиля, поскольку он излишне усложняет стилизацию (например, изменение цвета, изменение размера).

  • display:inline => не влияет на элементы SVG
  • opacity:1; fill:#000000; fill-opacity:1 => значения по умолчанию — можно безопасно удалить
  • stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-miterlimit:0;stroke-dasharray:none => stroke-width:0 отключает все обводки — последующие свойства не влияют на рендеринг
  • порядок рисования: маркеры заливки штрихов" => то же самое

Использование имен классов вместо идентификаторов помогает повторно использовать кнопки в разных стилях.


Или анимируйте SVG с помощью SVG SMIL <animateTransform> Анимация, CSS не нужен.

Оберните его в собственный веб-компонент JavaScript <svg-rotate-icon> для еще большего удобства использования.
Затем один HTML-тег создает один интерактивный значок.

<svg-rotate-icon id=ONE></svg-rotate-icon>
<svg-rotate-icon id=TWO></svg-rotate-icon>
<svg-rotate-icon id=THREE rotated></svg-rotate-icon>
<svg-rotate-icon id=FOUR disabled></svg-rotate-icon>

Обратите внимание на viewBox = "-50 -50 100 100", который делает начало координат 0,0 центром SVG, что упрощает вращение.

<style>
  svg-rotate-icon         { background:beige; width: 100px }
  svg-rotate-icon[rotated]{ background:green }
</style>

<script>
  customElements.define("svg-rotate-icon", class extends HTMLElement {
    connectedCallback() {
      let rotated = this.hasAttribute("rotated");
      let transform = `transform=rotate(${rotated ? 45 : 0})`;
      this
        .attachShadow({mode:"open"}) // required for use of :host and :host(), up to you to deleted it
        .innerHTML = `<style>:host{display:inline-block;cursor:pointer}</style>` +
        `<style>:host([disabled]){opacity:.5;pointer-events:none}</style>` +
        `<svg viewBox = "-50 -50 100 100">` +
        ` <path d = "M-35 0h70M0-35v70" ${transform} stroke=black stroke-width=10 stroke-linecap=round>` +
        `  <animateTransform attributeName=transform type=rotate dur=300ms begin=indefinite fill=freeze />` +
        ` </path>` +
        `</svg>`;
      let animElement = this.shadowRoot.querySelector("animateTransform");
      this.onclick = (evt) => {
        animElement.setAttribute('from', rotated ? 45 :  0);
        animElement.setAttribute('to'  , rotated ? 0  : 45);
        animElement.beginElement();
      }
      animElement.onend = (evt) => {
        this.toggleAttribute("rotated",rotated = !rotated);
        console.info(`rotated ${this.id}`, "dispatch Event here (with bubbles/composed properties)")
      }
    }
  })
</script>

<svg-rotate-icon id=ONE></svg-rotate-icon>
<svg-rotate-icon id=TWO></svg-rotate-icon>
<svg-rotate-icon id=THREE rotated></svg-rotate-icon>
<svg-rotate-icon id=FOUR disabled></svg-rotate-icon>