Draw SVG That Contains Multiple Paths, One Path At A Time, Animated With Javascript

538 Views Asked by At

I am new to SVG animations, I looked in to SNAP SVG and other libraries, but I cannot find a way to achieve my goal. I have a SVG file (or the SVG is directly integrated in HTML), that contains multiple paths. I want to show the SVG image, one path at a time, and animate each path like the example at:

To better describe the result I want, please take a look at this image: enter image description here

This is a svg element that contains 4 paths. I want to show draw each path using javascript (not CSS), one after another, and animate them like:

enter image description here

From left to right, each point at a time. To obtain something like a hand drawn effect of the image.

EDIT: Based on the answer from @ian, I made a jsfiddle showing what I have done so far. It displays all paths one by one, but I want somehow to simulate them as a drawing. So drawing the path between all points so it looks like in the animation above.

The current code works, in sense that it shows all paths, one by one, but it does not "draw them", it just instantly displays them, instead of creating an animation for each point path.

EDIT 2: I now understand the problem with my shape. They are not lines with points, but an object. I need to find a solution to turn the svg path object to a path with points.

https://jsfiddle.net/sodevrom/mvyru6g5/2/

var s = Snap("#svgout");
var svgin = Snap("#svgin");

function Drawing(svgin,transformString, timeBetweenDraws ) {
    this.fragment =svgin;
    this.pathArray = this.fragment.selectAll('path');
    this.group = s.g().transform( transformString ).drag();

    this.timeBetweenDraws = timeBetweenDraws;

};

Drawing.prototype.init = function( svgString, transformString ) {
      this.group.clear();
      this.currentPathIndex = 0;
};

Drawing.prototype.endReached = function() {
    if( this.currentPathIndex >= this.pathArray.length ) {
        return true;
    };
};

Drawing.prototype.callOnFinished = function() {
}

Drawing.prototype.initDraw = function() {
    this.init();
    this.draw();
};

Drawing.prototype.quickDraw = function() {
    this.init();
    this.timeBetweenDraws = 0;
    this.draw();
};

Drawing.prototype.draw = function() {

    if( this.endReached() ) {
        if( this.callOnFinished ) {
            this.callOnFinished();
            return
        };
    };
    var myPath = this.pathArray[ this.currentPathIndex ] ;

    this.leng = myPath.getTotalLength();
    

    this.group.append( myPath );

     myPath.attr({
      
       "stroke-dasharray": this.leng + " " + this.leng,
       "stroke-dashoffset": this.leng
     });

     this.currentPathIndex++;

     myPath.animate({"stroke-dashoffset": 2}, this.timeBetweenDraws, mina.easeout, this.draw.bind( this ) );

};



var myDrawing1=new Drawing( svgin, 'translate(0,400) scale(0.100000,-0.100000)', 800 );
myDrawing1.initDraw();

Any suggestions is appreciated.

Thank you

2

There are 2 best solutions below

7
winner_joiner On

As mentioned in the comments, the linked SO Answer is a good starting point. If you want to stagger the drawing a crude, option would be to set the timeout on each path. (you would have to match the delay with the animations speed). I did this here wit the style attribute.

Here a short Demo (CSS Version):

.paths path {
     --line-length: 100;
     --delay: 0s;
     --animation-speed: 1s;
       
     stroke-dasharray: var(--line-length);
     stroke-dashoffset: var(--line-length);
     animation: drawPath var(--animation-speed) linear 1 forwards;  
     animation-delay: var(--delay);
}

@keyframes drawPath {
   0% { stroke-dashoffset: var(--line-length); }
   100% { stroke-dashoffset: 0; }
}
<svg width="200" height="200" fill="none" xmlns="http://www.w3.org/2000/svg">
  <g class="paths">
    <path d="M0 50 L 100 50"  stroke="red" stroke-width="5"/>
    <path d="M0 100 L 100 100" style="--delay: 1s" stroke="red" stroke-width="5"/>
    <path d="M0 150 L 100 150" style="--delay: 2s" stroke="red" stroke-width="5"/>
  </g>
</svg>

Update

If you want to use javascript, there is a easy solution. Start the animation with javascript, by setting the animation-name, here a short demo. And so that all the path are drawn in succession, I just use a promise.

Here a short Demo (Javascript-Version):

let paths = document.querySelectorAll('.paths path');

function drawPath(path){
  return new Promise((resolve, reject) => {
      path.addEventListener("animationend", resolve);
      path.style.setProperty('animation-name', 'drawPath');
  });
}

// Just for the demo, I'm starting all path's in a loop.
let promise = Promise.resolve();
for(let path of paths){
    promise  = promise.then( () => drawPath(path));
}
.paths path {
     --line-length: 100;
     --delay: 0.5s;
     --animation-speed: 1s;
       
     stroke-dasharray: var(--line-length);
     stroke-dashoffset: var(--line-length);
     animation-delay: var(--delay); 
     animation-duration: var(--animation-speed); 
     animation-fill-mode: forwards;
     animation-iteration-count: 1;
     animation-timing-function: linear; 

}
@keyframes drawPath {
   0% { stroke-dashoffset: var(--line-length); }
   100% { stroke-dashoffset: 0; }
}
<svg width="200" height="200" fill="none" xmlns="http://www.w3.org/2000/svg">
  <g class="paths">
    <path d="M0 50 L 100 50" stroke="red" stroke-width="5"/>
    <path d="M0 100 L 100 100" stroke="red" stroke-width="5"/>
    <path d="M0 150 L 100 150" stroke="red" stroke-width="5"/>
  </g>
</svg>

Info: this will only work, if the lines are real lines, not objects with filling.

Update 2:

I had a bit of time and rewrote/re-drew the svg, so that the code could work, with javascript and css animations. The image doesn't look anymore as good, but it could be tweaked.

let groups = [
    {name:'#board', animate: false},
    {name:'#note', animate: true},
    {name:'#lines1', animate: true},
    {name:'#chart1', animate: true},
    {name:'#chart2', animate: true},
    {name:'#circle1', animate: true},
    {name:'#circle2', animate: false},
    {name:'#lines2', animate: true},
];

function drawPath(path, shouldAnimate){
  return new Promise((resolve, reject) => {
    if(shouldAnimate){
        path.addEventListener("animationend", resolve);
        path.style.setProperty('animation-name', 'drawPath');
    } else {
        path.style.setProperty('stroke-dashoffset', '0');
        resolve();
    }   
  });
}

let promise = Promise.resolve();
for(let groupName of groups){
    let paths = document.querySelectorAll(`${groupName.name} path`);
    for(let path of paths){
        promise  = promise.then( () => drawPath(path, groupName.animate));
    }
}
path {
  fill: none;
  stroke: black;
  stroke-width: 3;
}
      
g path {
   --line-length: 1000;
   --delay: .5s;
   --animation-speed: .5s;

   stroke-dasharray: var(--line-length);
   stroke-dashoffset: var(--line-length);
   animation-delay: var(--delay); 
   animation-duration: var(--animation-speed); 
   animation-fill-mode: forwards;
   animation-iteration-count: 1;
   animation-timing-function: ease-in; 
}

@keyframes drawPath {
   0% { stroke-dashoffset: var(--line-length); }
   100% { stroke-dashoffset: 0; }
}
<svg version="1.0" width="160pt" height="250pt" id="svgin" xmlns="http://www.w3.org/2000/svg"
    xmlns:svg="http://www.w3.org/2000/svg">
    <g transform="translate(-98.084291,-32.694764)" id="board">
        <path d="m 128.59771,65.502953 145.17295,-4.950374 -10.41368,152.307811 10.69581,0.007 0.22371,16.11072 -148.46179,-2.84852 -2.17446,-8.91033 151.39294,2.59059 -0.54008,-6.54345 -145.67879,-5.15697 -5.23251,8.92744 4.76799,-8.74505 8.91742,-0.46453 z" id="path14" />
        <path d="m 190.71946,63.346105 2.04342,-11.238826 c 4.65482,0.907554 9.67602,-3.764323 13.96338,1e-6 l 2.384,10.217114 z" id="path15" />
        <path d="m 154.61899,226.47936 20.43422,0.34056 -37.12218,116.4751 -17.36909,-0.34057 z" id="path16" />
        <path d="m 192.76288,227.1605 5.44913,67.43294 11.23882,3.74628 7.49255,-71.17922 z" id="path17" />
        <path d="m 237.03704,228.52278 34.39761,117.4968 12.26054,2.384 5.78969,-6.81141 -35.41932,-113.06939 z" id="path18" />
    </g>
    <g transform="translate(-98.084291,-32.694764)" id="note">
        <path d="m 143.72073,79.693487 42.57131,-2.724564 -4.08685,42.911877 -34.73818,3.4057 z" id="path19" style="--line-length: 300;" />
        <path d="m 168.92295,72.882078 5.44912,-1.021712 v 15.325671 l -6.13026,0.681141 z" id="path20" style="--line-length: 100;" />
    </g>
    <g transform="translate(-98.084291,-32.694764)" id="lines1">
        <path d="m 199.23372,80.715198 56.19412,-2.043423 v 0 z" id="path21" />
        <path d="m 198.55258,95.700298 27.92677,-2.724564" id="path22" />
        <path d="m 196.84972,109.32312 22.47765,-2.384" id="path23" />
    </g>
    <g transform="translate(-98.084291,-32.694764)" id="chart2">
        <path d="m 143.03959,148.48872 15.32567,19.07194 13.62282,-35.41932 15.66624,29.9702 28.26735,-37.46275 20.43423,22.47765 23.83993,-20.09366 -4.42742,14.9851 -12.26053,-12.60111 14.9851,-3.4057" id="path25" />
    </g>
    <g transform="translate(-96.639373,-33.417223)" id="lines2">
        <path d="m 149.16986,180.50234 26.90507,1.70285" id="path26" />
        <path d="m 192.42231,179.8212 58.57812,2.72456" id="path27" />
        <path d="m 151.55385,197.87143 98.08429,1.36229" id="path28" />
    </g>
    <g transform="translate(-98.084291,-32.694764)" id="circle1">
        <path d="m 216.84634,154.57013 c -9.04033,2.26643 -11.0175,13.0598 0.60268,14.05192 8.33693,-2.77932 9.38648,-12.18918 -0.60268,-14.05192 z" id="path29" />
    </g>
    <g transform="translate(-98.084291,-32.694764)" id="chart1">
        <path d="m 147.80758,166.87952 11.91996,-33.03533 12.94168,28.60792 20.43423,-45.29587 18.3908,33.03533 42.23074,-58.237547 -18.05023,9.876547 18.73137,-10.557688 -1.02171,20.434228 -19.75309,-7.83312" id="path24" />
    </g>
    <g id="circle2">
        <path d="m 247.02185,170.9168 c 9.59854,-2.57543 11.6978,-14.84031 -0.6399,-15.96768 -8.85171,3.15823 -9.96606,13.85098 0.6399,15.96768 z" id="path29-0" transform="translate(-98.084291,-32.694764)" />
    </g>
</svg>

2
Ian On

We can use the dash offset technique with Snap, which animates stroke-dashoffset. Example, including using prototypes with Snap is below, it will also allow chaining of animations.

You can use an existing element rather than the string example below, using

var element = Snap( someCSSSelector );

You can run below example code below from tutorial site (my original code)

var s = Snap("#svgout");

var svgString1 = '<path id="s3" d="M 60 0 L 120 0 L 180 60 L 180 120 L 120 180 L 60 180 L 0 120 L 0 60 Z"  stroke="blue"/>';
var svgString2 = '<path id="s3" d="M 60 0 L 120 0 L 180 60 L 180 120 L 120 180 L 60 180 L 0 120 L 0 60 Z"  stroke="red"/>';


function Drawing( svgString, transformString, timeBetweenDraws ) {
    this.fragment = Snap.parse( svgString );
    this.pathArray = this.fragment.selectAll('path');
    this.group = s.g().transform( transformString ).drag();
    this.timeBetweenDraws = timeBetweenDraws;
};

Drawing.prototype.init = function( svgString, transformString ) {
      this.group.clear();
      this.currentPathIndex = 0;

};

Drawing.prototype.endReached = function() {
    if( this.currentPathIndex >= this.pathArray.length ) {
        return true;
    };
};

Drawing.prototype.callOnFinished = function() {
}

Drawing.prototype.initDraw = function() {
    this.init();
    this.draw();
};

Drawing.prototype.quickDraw = function() {
    this.init();
    this.timeBetweenDraws = 0;
    this.draw();
};

Drawing.prototype.draw = function() {         // this is the main animation bit
    if( this.endReached() ) {
        if( this.callOnFinished ) {
            this.callOnFinished();
            return
        };
    };
    var myPath = this.pathArray[ this.currentPathIndex ] ;

    this.leng = myPath.getTotalLength();

    this.group.append( myPath );

     myPath.attr({
       fill: 'none',
       "stroke-dasharray": this.leng + " " + this.leng,
       "stroke-dashoffset": this.leng
     });

     this.currentPathIndex++;

     myPath.animate({"stroke-dashoffset": 0}, this.timeBetweenDraws, mina.easeout, this.draw.bind( this ) );

};



var myDrawing1 = new Drawing( svgString1, 't0, 0, s1.8', 800 );
var myDrawing2 = new Drawing( svgString2, 't69,50 s1.8', 3000 );
var myDrawing3 = new Drawing( svgString2, 't150,150 s1.8', 5000 );

myDrawing1.initDraw();
myDrawing1.callOnFinished = function() { myDrawing2.initDraw() };
myDrawing2.callOnFinished = function() { myDrawing3.initDraw() };