Home > database >  How to morph svg into another svg correctly with Anime.js?
How to morph svg into another svg correctly with Anime.js?

Time:05-20

I have a problem where two of my svg have the same number of points, but something isn't right when I play the animation, the two svgs are so close together but the animation just jumps out of nowhere and it isn't right, a weird shape happens before the first svg changes to the second.

I'm doing the svgs in Adobe XD. Here is the code:

<svg id="morph" viewBox="0 0 1920 540">
   <path  d="m864.216 135.95 36.39 41.917S780.519 307.11 1078.914 373.479s221.979-87.327 221.979-87.327l32.75-34.931s25.473 101.3 207.422 34.931 440.314 150.2 411.2 380.744S34.528 576.079 34.528 576.079s-3.64-429.647 342.063-509.987 272.923 174.653 487.623 69.861"/>
</svg>

 <script>
            var overlay = document.getElementById('morph');

            var morphing = anime({
                targets: '.morph',
                d: [
                    {value : "m864.216 135.95 36.39 41.917S780.519 307.11 1078.914 373.479s221.979-87.327 221.979-87.327l32.75-34.931s25.473 101.3 207.422 34.931 440.314 150.2 411.2 380.744S34.528 576.079 34.528 576.079s-3.64-429.647 342.063-509.987 272.923 174.653 487.623 69.861"},
                    {value: "M2.49 576.483S20.535 398.736 122.472 239.61s236.674-199.127 302-217.883c176.407-41.244 334 45.685 334 45.685l340 233.7s172 105.427 280 119.484 322 12.3 322 12.3 118 5.271 160 61.5 56 89.613 62 117.727S2.49 576.483 2.49 576.483Z"},
                ],
                duration: 1200,
                loop: false,
                easing: 'easeInOutQuint'
            })
        </script>

CodePudding user response:

Your paths still need some optimizations to be fully compatible for interpolation.

Most animation libraries try to make paths compatible to some extent (e.g by converting them to polygons like in flubber.js).

But usually you'll get the best results cleaning up your paths manually.

step 1 step 2
m 864.216 135.95 M 2.49 576.483
l 36.39 41.917 S 20.535 398.736 122.472 239.61
S 780.519 307.11 1078.914 373.479 s 236.674 -199.127 302-217.883
s 221.979 -87.327 221.979 -87.327 c 176.407 -41.244 334 45.685 334 45.685
l 32.75 -34.931 l 340 233.7
s 25.473 101.3 207.422 34.931 s 172 105.427 280 119.484
s 440.314 150.2 411.2 380.744 s 322 12.3 322 12.3
S 34.528 576.079 34.528 576.079 s 118 5.271 160 61.5
s -3.64 -429.647 342.063-509.987 s 56 89.613 62 117.727
s 272.923 174.653 487.623 69.861 S 2.49 576.483 2.49 576.483
Z

Your command types also need to be compatible.

E.g. The second animation step has only one l (lineTo) command and a Z (closePath) missing in the first path.

Unfortunately, you can't be sure your editor/graphic app will output the same commands as it might decide to use shorthand commands (like h for horizontal lineTos )to minify the markup.

Normalize d to a reduced set of commands

This will simplify the further adjustments by converting path data to M, C, L and Z commands.
I'm using Jarek Foksa's getPathData polyfill.

path.getPathData({normalize: true});

{normalize: true} Parameter will also convert all commands to absolute coordinates.

Convert L commands to C

You can easily convert L commands to C curves by repeating the x/y coordinates like this.

L 901 178   

to:

C 901 178 901 178 901 178

Adjust M starting points

Set the starting point to something like the leftmost corner/point. So your paths will be interpolated using a visual reference point. Otherwise you might get weird flipping transitions.

Changing the M of a path is also way easier with absolute and normalized commands. You'll also find a helper function in the snippet

(1. path data chunk => will be appended after the second chunk)
M 864 136 (old starting point => will be deleted)
C 901 178 901 178 901 178
C 901 178 781 307 1079 373
C 1377 440 1301 286 1301 286
C 1334 251 1334 251 1334 251
C 1334 251 1359 353 1541 286
C 1723 220 1981 436 1952 667
C 1923 897 35 576 35 576 => will become the new M xy coordinate

(2. path data chunk)
C 35 576 31 146 377 66
C 722 -14 650 241 864 136

(3. path data chunk: closePath)
Z

Result:

M 35 576  
C 35 576 31 146 377 66   
C 722 -14 650 241 864 136   

C 901 178 901 178 901 178   
C 901 178 781 307 1079 373   
C 1377 440 1301 286 1301 286   
C 1334 251 1334 251 1334 251   
C 1334 251 1359 353 1541 286   
C 1723 220 1981 436 1952 667   
C 1923 897 35 576 35 576  

Z   

Path direction

In your case both paths have a clockwise direction.
If you encounter weird flipping transitions you can try to reverse path directions.
You might use @enxaneta's great codepen example or Svg Path commander Library.

Example 1: Normalize and change starting point (including helpers)

let svgNorm = document.querySelectorAll('.svgNorm');

svgNorm.forEach(function(svg) {
  let svgPaths = svg.querySelectorAll('path');
  normalizePaths(svgPaths, 0, true);
})

let orig1 = document.querySelector('.orig1');
let orig2 = document.querySelector('.orig2');

let path1 = document.querySelector('.morph1');
let path2 = document.querySelector('.morph2');

//shift starting point
shiftSvgStartingPoint(path1, 7);

//show starting points
addMarkers(orig1);
addMarkers(orig2);
addMarkers(path1);
addMarkers(path2);


function normalizePaths(paths, decimals = 1, convertLineto = false) {
  paths.forEach(function(path, i) {
    let pathData = path.getPathData({
      normalize: true
    });
    pathData.forEach(function(com) {
      let [type, values] = [com['type'], com['values']];
      values.forEach(function(coord, c) {
        com['values'][c] =  (com['values'][c]).toFixed(decimals)
      })
      let [x, y] = [com['values'][0], com['values'][1]];
      if (type == 'L' && convertLineto) {
        com['type'] = 'C';
        com['values'] = [x, y, x, y, x, y];
      }
    })
    path.setPathData(pathData)
  })
}

function shiftSvgStartingPoint(path, offset) {
  let pathData = path.getPathData({
    normalize: true
  });
  let pathDataL = pathData.length;

  //exclude Z/z (closepath) command if present
  let lastCommand = (pathData[pathDataL - 1]['type']);
  let trimR = 0;
  if (lastCommand == 'Z') {
    trimR = 1;
  }

  let newStartIndex = offset   1 < pathData.length - 1 ? offset   1 : pathData.length - 1 - trimR;
  let newPathData = pathData;
  let newPathDataL = newPathData.length;

  // slice array to reorder
  let newPathDataStart = newPathData.slice(newStartIndex);
  let newPathDataEnd = newPathData.slice(0, newStartIndex);
  // remove original M
  newPathDataEnd.shift();

  let newPathDataEndL = newPathDataEnd.length;
  let newPathDataEndLastValues = newPathDataEnd[newPathDataEndL - 1]['values'];
  let newPathDataEndLastXY = [newPathDataEndLastValues[newPathDataEndLastValues.length - 2],
    newPathDataEndLastValues[newPathDataEndLastValues.length - 1]
  ];

  //remove z(close path) from original pathdata array
  if (trimR) {
    newPathDataStart.pop();
    newPathDataEnd.push({
      'type': 'Z',
      'values': []
    });
  }

  // prepend new M command and concatenate array chunks
  newPathData = [{
    'type': 'M',
    'values': newPathDataEndLastXY
  }].concat(newPathDataStart).concat(newPathDataEnd);

  // update path's d property
  path.setPathData(newPathData);
  return path;
}


testInterpolation(path1, path2);

function testInterpolation(path1, path2) {
  path1.addEventListener('click', function(e) {
    if (!path1.getAttribute('style')) {
      path1.setAttribute('style', `d:path("${path2.getAttribute('d')}")`)
    } else {
      path1.removeAttribute('style');
    }
  })

}

function addMarkers(path) {
  let svg = path.closest('svg');
  let markerDef = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
  let marker =
    `<marker id="circle" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="10%" markerHeight="10%"
                    orient="auto-start-reverse">
                <circle cx="5" cy="5" r="5" fill="green" />
            </marker>`;
  markerDef.innerHTML = marker;
  svg.insertBefore(markerDef, svg.childNodes[0]);
  path.setAttribute('marker-start', 'url(#circle)');
}
svg {
  display: inline-block;
  width: 30%;
  overflow: visible;
  border: 1px solid #ccc;
  margin-right: 5%;
}

.row {
  margin-top: 4em;
}

path {
  opacity: 0.5;
  transition: 0.5s;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/path-data-polyfill.min.js"></script>

<div>
<p>Green points illustrate starting points</p>

<svg viewBox="0 0 1920 540">
        <path  d="
            m 864.216 135.95 
            l 36.39 41.917
            S 780.519 307.11 1078.914 373.479
            s 221.979 -87.327 221.979 -87.327
            l 32.75 -34.931
            s 25.473 101.3 207.422 34.931 
            s 440.314 150.2 411.2 380.744
            S 34.528 576.079 34.528 576.079
            s -3.64 -429.647 342.063-509.987 
            s 272.923 174.653 487.623 69.861
            z" />
    </svg>
 

<svg viewBox="0 0 1920 540">
        <path  d="
            M 2.49 576.483
            S 20.535 398.736 122.472 239.61
            s 236.674 -199.127 302-217.883
            c 176.407 -41.244 334 45.685 334 45.685
            l 340 233.7 
            s 172 105.427 280 119.484 
            s 322 12.3 322 12.3 
            s 118 5.271 160 61.5 
            s 56 89.613 62 117.727
            S 2.49 576.483 2.49 576.483
            Z" />
    </svg>
</div>
<div >
  <svg  viewBox="0 0 1920 540">
        <path 
            d="
            m 864.216 135.95 
            l 36.39 41.917
            S 780.519 307.11 1078.914 373.479
            s 221.979 -87.327 221.979 -87.327
            l 32.75 -34.931
            s 25.473 101.3 207.422 34.931 
            s 440.314 150.2 411.2 380.744
            S 34.528 576.079 34.528 576.079
            s -3.64 -429.647 342.063-509.987 
            s 272.923 174.653 487.623 69.861
            z" />
    </svg>

  <svg  viewBox="0 0 1920 540">
        <path 
            d="
            M 2.49 576.483
            S 20.535 398.736 122.472 239.61
            s 236.674 -199.127 302-217.883
            c 176.407 -41.244 334 45.685 334 45.685
            l 340 233.7 
            s 172 105.427 280 119.484 
            s 322 12.3 322 12.3 
            s 118 5.271 160 61.5 
            s 56 89.613 62 117.727
            S 2.49 576.483 2.49 576.483
            Z" />
    </svg>

<p>Click on the left path to see morphing animation. <br />Inspect this path in DevTools to get new compatible path data.</p>

</div>

Example 2: morph optimized paths with anime.js

var morphing = anime({
  targets: ".morph",
  d: [
    {
      value:
        "M 2 576 C 2 576 21 399 122 240 C 224 80 359 40 424 22 C 601 -20 758 67 758 67 C 1098 301 1098 301 1098 301 C 1098 301 1270 407 1378 421 C 1486 435 1700 433 1700 433 C 1700 433 1818 438 1860 494 C 1902 551 1916 584 1922 612 C 1928 640 2 576 2 576 Z"
    }
  ],
  duration: 1200,
  loop: false,
  easing: "easeInOutQuint"
});
svg{
  display:inline-block;
  width:20em;
  overflow:visible;
}

.morph{
  transition:0.5s;
}

.morphPoly{
    transition:0.5s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<svg id="morph" viewBox="0 0 1920 540">
<path  d="M 35 576 C 35 576 31 146 377 66 C 722 -14 650 241 864 136 C 901 178 901 178 901 178 C 901 178 781 307 1079 373 C 1377 440 1301 286 1301 286 C 1334 251 1334 251 1334 251 C 1334 251 1359 353 1541 286 C 1723 220 1981 436 1952 667 C 1923 897 35 576 35 576 Z" />
</svg>

... Quite a lot of work.
But once your paths are super compatible you can also morph between shapes via plain css. (E.g by animating/transitioning d:path() properties).

  • Related