javascripthtmlsvg

How to reverse SVG animations with JS in HTML


I am creating a website to simulate lever frames for railways that currently use them. To simulate the levers I decided to use SVGs which seems to be working relatively well. However, I am currently running into the issue not being able to reverse the animations so reset the levers to their original state.

The animation worked fine (other than the text moving too far down) with both keyPoints="0; 1" and keyPoints="1; 0" while using beginElement() to trigger the animation, but as I have it set up right now there is no animation at all.

How can I get the animation to play in reverse so the levers can move either direction? Is there a better way than reversing the keyPoints?

//defined in a json file
leverframe = {
  "1": {
    "id": "HJ1",
    "type": "d"
  },
  "2": {
    "id": "HJ2",
    "type": "s"
  },
  "3": {
    "id": "HJ3",
    "type": "s"
  },
  "4": {
    "id": "HJ4",
    "type": "es"
  }
}

// box.html
const leverlist = new Map()

for (const lever of Object.keys(leverframe)) {
  const leverimg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  levers.appendChild(leverimg)
  leverimg.outerHTML = '<svg xmlns="http://www.w3.org/2000/svg" id="' + lever + '" viewBox="0 0 35 300" width="35px" height="300px"><image href="https://cdn.glitch.global/35ec23e6-47cb-4d5f-87eb-598ad1888739/' + leverframe[lever].type + '.png?v=1749249192147" height="329" y="-0.579" style="transform-box: fill-box; transform-origin: 50% 50%;"><title>image</title><animateTransform id="' + lever + 'pull1" type="scale" additive="sum" attributeName="transform" values="1 1;1 0.9" begin="indefinite" dur="2s" fill="freeze" keyTimes="0; 1" keyPoints="0; 1"></animateTransform><animateMotion id="' + lever + 'pull2" path="M 0 0 L 0 34.352" calcMode="linear" begin="pull1.begin" dur="2s" fill="freeze" keyTimes="0; 1" keyPoints="0; 1"></animateMotion></image><text style="white-space: pre; fill: rgb(51, 51, 51); font-family: Arial, sans-serif; font-size: 10px; text-anchor: middle;" x="12.5" y="117.5" id="object-0"><title>text</title>' + lever + '<animateMotion id="' + lever + 'pull3" path="M 0 0 L 0 43.512" calcMode="linear" begin="pull1.begin" dur="2s" fill="freeze" keyTimes="0; 1" keyPoints="0; 1"></animateMotion></text></svg>'

  leversvg = document.getElementById(lever)
  leverlist.set(lever, false)

  leversvg.onclick = function() {
    leverlist.set(lever, !leverlist.get(lever))

    if (leverlist.get(lever)) {
      console.log("doing animation")
      console.log(document.getElementById(lever + "pull1"))
      document.getElementById(lever + 'pull1').setAttribute("keyPoints", "0; 1")
      document.getElementById(lever + 'pull2').setAttribute("keyPoints", "0; 1")
      document.getElementById(lever + 'pull3').setAttribute("keyPoints", "0; 1")
      document.getElementById(lever + 'pull1').beginElement();
      console.log("done animation")
    } else {
      console.log("doing animation")
      console.log(document.getElementById(lever + "pull1"))
      document.getElementById(lever + 'pull1').setAttribute("keyPoints", "1; 0")
      document.getElementById(lever + 'pull2').setAttribute("keyPoints", "1; 0")
      document.getElementById(lever + 'pull3').setAttribute("keyPoints", "1; 0")
      document.getElementById(lever + 'pull1').beginElement();
      console.log("done animation")
    }
  }
}
<div id="levers" alt="Box Levers" style="max-width: 90vw; overflow-y: auto; white-space: nowrap;"></div>


Solution

  • AI suggested to put the <image> inside <g> because animateTransform doesn't work on images in all browsers.
    Once I deleted that animateTransform everything works (Chromium/FireFox)

    I then manually cleaned your code (viewBox dimensions, heights, typos, redundancies);
    wrote a native JavaScript Web Component JSWC so all you have to use in HTML is:

    <svg-track-lever id="HJ1" type="d" label="1"></svg-track-lever>
    <svg-track-lever id="HJ2" label="2"></svg-track-lever>
    <svg-track-lever id="HJ3" type="s" leverstate="0"></svg-track-lever>
    <svg-track-lever id="HJ4" type="es" label="4" dur="2"></svg-track-lever>
    

    to create:

    key takeaway: Use shadowDOM so CSS & ID values are scoped
    easier to work with than creating unique IDs in the Document.

    All code required:

    <script>
      customElements.define("svg-track-lever", class extends HTMLElement {
        constructor() {
          super().attachShadow({ mode: "open" }) // shadowDOM so all HTML, ID values and CSS are scoped
        }
        connectedCallback() {
          this.leverstate = this.getAttribute("leverstate") || 1; // keyPoints[direction]
          this.shadowRoot.innerHTML =
          `<style>` +
            `  :host{display:inline-block;height:300px;background:beige}` + // :host setting so user can overrule styled shadowDOM with CSS
            `  svg  {cursor:pointer;height:inherit}` +
            `  text {font:35px arial}` +
            `</style>` +
            `<svg part="svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 600">` +
            `<g>` +
            `  <image part="image" href="https://cdn.glitch.global/35ec23e6-47cb-4d5f-87eb-598ad1888739/${this.getAttribute("type") || "d"}.png?v=1749249192147"/>` +
            `  <text part="text" x="20" y="245">${this.getAttribute("label") || 0}</text>` +
            `  <animateMotion id="ANIM" path="M0 0L0 123" dur="${this.getAttribute("dur") || 1}s" fill="freeze" keyTimes="0;1"/>` +
            `</g>` +
            `</svg>`;
          this.onclick = (evt) => {
            this.levertoggle();
          }
          this.levershift(this.leverstate); // initial lever state
        }
        levershift(state = 0) { // callable method
          this.shadowRoot.getElementById("ANIM").setAttribute("keyPoints", ["0;1", "1;0"][state]);
          this.shadowRoot.getElementById("ANIM").beginElement();
        }
        levertoggle() { // callable method
          this.levershift(this.leverstate = 1 - this.leverstate);
        }
      })
    </script>
    <style>
        svg-track-lever { height:200px }
        svg-track-lever::part(text) { font-weight:bold; fill:darkred }
    </style>
    <svg-track-lever id="HJ1" type="d" label="1"></svg-track-lever>
    <svg-track-lever id="HJ2" label="2"></svg-track-lever>
    <svg-track-lever id="HJ3" type="s" leverstate="0"></svg-track-lever>
    <svg-track-lever id="HJ4" type="es" label="4" dur="2"></svg-track-lever>