Search code examples
javascriptsvgcss-animationssvg-animateweb-animations-api

stack/add/composite/accumulate multiple transform=translate animations in the Web Animation API


I have been running around in circles

I use the Web Animation API transform:translate() to make my player run around the bases.

runto("first").then(() => runto("second")); does the correct animation,
but then deletes the first/previous translate to first base,
thus my player ends up in the wrong location

I have tried every combination of

Is it at all possible to preserve all the translates on one element?

JSFiddle playground: https://jsfiddle.net/WebComponents/8df2qsop/


  let field = document.getElementById("FIELD");
  let player = field.getElementById("player");
  function getCenter(el , { left, top, width, height } = el.getBoundingClientRect()) {
    let point = Object.assign(field.createSVGPoint(),{  x : left + width / 2, y : top + height / 2 }).matrixTransform(field.getScreenCTM().inverse());
    return { cx:~~point.x, cy:~~point.y };
  }
  function runto(destination) {
    let {cx:playerX,cy:playerY} = getCenter(player);
    let {cx:baseX  ,cy:baseY  } = getCenter(field.getElementById(destination));
    let translateX = baseX - playerX;  
    let translateY = baseY- playerY;
    let track = `<line x1="${playerX}" y1="${playerY}" x2="${baseX}" y2="${baseY}" stroke-width="10" stroke="black"/>`;
    field.insertAdjacentHTML("beforeend",track);
    return player.animate([{ 
          //transform: `translate(${0}px,${0}px)` 
          }, { transform: `translate(${translateX}px,${translateY}px)` }], 
        { duration:500, fill:"forwards", composite:"add" }
    ).finished; // return Promise
  }
  runto("first").then(() => runto("second"));
<style>
  #FIELD { background: lightgreen; width: 150px; margin: 20px }
</style>
<svg id=FIELD viewBox="0 0 250 250" transform="rotate(45)">
  <g fill="brown">
    <path id="home"   d="M 150 150 h 80 v 80 h -80 v -80z" fill="green"/>
    <path id="first"  d="M 150 20  h 80 v 80 h -80 v -80z"/>
    <path id="second" d="M  20 20  h 80 v 80 h -80 v -80z"/>
    <path id="third"  d="M 20 150  h 80 v 80 h -80 v -80z"/>
  </g>
  <circle id="player" cx="190" cy="190" r="30" fill="gold"/>
</svg>


Solution

  • You could call Animation#commitStyles() after the animation completes, so that its state is "written" to your player.
    You now need to come back to the default composite: "replace" option, and you also need to modify your initial player settings so that it's positioned using CSS translate() instead of using its cx and cy values (or you could also modify your calculations so they return relative positions instead).

    let field = document.getElementById("FIELD");
    let player = field.getElementById("player");
    
    function getCenter(el, { left, top, width, height } = el.getBoundingClientRect()) {
      let point = Object.assign(field.createSVGPoint(), {
        x: left + width / 2,
        y: top + height / 2
      }).matrixTransform(field.getScreenCTM().inverse());
      return {
        cx: ~~point.x,
        cy: ~~point.y
      };
    }
    
    async function runto(destination) {
      let { cx: playerX, cy: playerY } = getCenter(player);
      let { cx: baseX, cy: baseY } = getCenter(field.getElementById(destination));
      let translateX = baseX - playerX;
      let translateY = baseY - playerY;
      let track = `<line x1="${playerX}" y1="${playerY}" x2="${baseX}" y2="${baseY}" stroke-width="10" stroke="black"/>`;
      field.insertAdjacentHTML("beforeend", track);
      const anim = player.animate([{
        transform: `translate(${baseX}px,${baseY}px)`
      }], {
        duration: 500,
        fill: "forwards"
      });
      await anim.finished;
      anim.commitStyles(); // write the current state to the animated element
      anim.cancel(); // no need to keep it around anymore
    }
    runto("first").then(() => runto("second"));
    <style>
      #FIELD {
        background: lightgreen;
        width: 150px;
        margin: 20px
      }
    </style>
    <svg id=FIELD viewBox="0 0 250 250" transform="rotate(45)">
      <g fill="brown">
        <path id="home"   d="M 150 150 h 80 v 80 h -80 v -80z" fill="green"/>
        <path id="first"  d="M 150 20  h 80 v 80 h -80 v -80z"/>
        <path id="second" d="M  20 20  h 80 v 80 h -80 v -80z"/>
        <path id="third"  d="M 20 150  h 80 v 80 h -80 v -80z"/>
      </g>
      <circle id="player" cx="0" cy="0" r="30" style="transform:translate(190px, 190px)" fill="gold"/>
    </svg>