1

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

BlasterGod
  • 196
  • 1
  • 13
  • 1
    See https://stackoverflow.com/questions/37779906/how-to-animate-handwriting-text-on-the-web-page-using-svg – Robert Longson Aug 03 '23 at 21:09
  • @RobertLongson I want to achieve this using JavaScript not css , to have more control over everything. Also I prefer to load the svg from a file instead of it being embedded in the HTML document – BlasterGod Aug 03 '23 at 21:31
  • 1
    create the css properties with javascript then. – Robert Longson Aug 03 '23 at 22:36
  • 1
    Please show the code you wrote and tried, it now seems you want us to do your job. You can help us answering your question, by adding a [minimal-reproducible-example StackOverflow Snippet](https://stackoverflow.com/help/minimal-reproducible-example). It will help readers execute your code with one click. And help create answers with one click. Thank you. – Danny '365CSI' Engelman Aug 04 '23 at 05:43

2 Answers2

2

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>
winner_joiner
  • 12,173
  • 4
  • 36
  • 61
  • Thank you. This is the desired effect, but I want to make it using Javascript, so I can control it a bit in the future. The idea would be at some point to add a pencil over it, and make it look like somebody is drawing the SVG with a pencil – BlasterGod Aug 05 '23 at 15:58
  • 1
    I updated my answer, but it might not work on your svg-image from the jsfiddle. since your lines are constructed with infill, and are not real singular lines. – winner_joiner Aug 06 '23 at 07:46
  • Hello, your last edit is exactly what I wanted to obtain. The problem is that it only works for certain paths. What I have is this. I have a bmp image. I use potrace to convert it to svg. Then I want to draw the obtained svg like you did. I cannot re write the svg manually. Any suggestions? Thank you very much for your time – BlasterGod Aug 07 '23 at 12:57
  • I used the free tool [inkscape](https://inkscape.org/) and just repainted it, with the _"node paths"_ tool, and edited the svg/xml in an editor to add css classes / styles. I hope this helps. Just use the paths `stroke` and set `fill`property to _none_. – winner_joiner Aug 07 '23 at 19:55
  • Thanks for taking the time to make this work. It works perfect. I just need to find a way to automate bmp to svg conversion in the form you made. I want it to work with any black and white bmp. I will try autotrace instead of potrace to convert bmp to svg, to see if I obtain better results. – BlasterGod Aug 08 '23 at 07:39
0

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() };
Ian
  • 13,724
  • 4
  • 52
  • 75
  • I tried to implement it, but it does not work. I made a jsfiddle at https://jsfiddle.net/sodevrom/mvyru6g5/2/ I have the original svg on the right, and the svgout on the left. I would like to take all paths from svgin and draw them in svgout one by one. I am missing something I think – BlasterGod Aug 05 '23 at 16:19
  • You need to call initDraw() on it, eg var d = new Drawing( svgin, 't0, 0, s1.8', 800 ); d.initDraw(); (and you can chain like the end of the example code). – Ian Aug 08 '23 at 13:21